package api import ( "fmt" "net/http" "strings" "github.com/aptly-dev/aptly/aptly" "github.com/aptly-dev/aptly/deb" "github.com/aptly-dev/aptly/pgp" "github.com/aptly-dev/aptly/task" "github.com/aptly-dev/aptly/utils" "github.com/gin-gonic/gin" ) type signingParams struct { // Don't sign published repository Skip bool ` json:"Skip" example:"false"` // GPG key ID to use when signing the release, if not specified default key is used GpgKey string ` json:"GpgKey" example:"A0546A43624A8331"` // GPG keyring to use (instead of default) Keyring string ` json:"Keyring" example:"trustedkeys.gpg"` // GPG secret keyring to use (instead of default) Note: depreciated with gpg2 SecretKeyring string ` json:"SecretKeyring" example:""` // GPG passphrase to unlock private key (possibly insecure) Passphrase string ` json:"Passphrase" example:"verysecure"` // GPG passphrase file to unlock private key (possibly insecure) PassphraseFile string ` json:"PassphraseFile" example:"/etc/aptly.passphrase"` } type sourceParams struct { // Name of the component Component string `binding:"required" json:"Component" example:"main"` // Name of the local repository/snapshot Name string `binding:"required" json:"Name" example:"snap1"` } func getSigner(options *signingParams) (pgp.Signer, error) { if options.Skip { return nil, nil } signer := context.GetSigner() signer.SetKey(options.GpgKey) signer.SetKeyRing(options.Keyring, options.SecretKeyring) signer.SetPassphrase(options.Passphrase, options.PassphraseFile) // If Batch is false, GPG will ask for passphrase on stdin, which would block the api process signer.SetBatch(true) err := signer.Init() if err != nil { return nil, err } return signer, nil } // Replace '_' with '/' and double '__' with single '_', SanitizePath func slashEscape(path string) string { result := strings.Replace(strings.Replace(path, "_", "/", -1), "//", "_", -1) result = utils.SanitizePath(result) if result == "" { result = "." } return result } // @Summary List Published Repositories // @Description **Get list of published repositories** // @Description // @Description Return list of published repositories including detailed information. // @Description // @Description See also: `aptly publish list` // @Tags Publish // @Produce json // @Success 200 {array} deb.PublishedRepo // @Failure 500 {object} Error "Internal Error" // @Router /api/publish [get] func apiPublishList(c *gin.Context) { collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() repos := make([]*deb.PublishedRepo, 0, collection.Len()) err := collection.ForEach(func(repo *deb.PublishedRepo) error { err := collection.LoadShallow(repo, collectionFactory) if err != nil { return err } repos = append(repos, repo) return nil }) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, err) return } c.JSON(http.StatusOK, repos) } // @Summary Show Published Repository // @Description **Get published repository information** // @Description // @Description Show detailed information of a published repository. // @Description // @Description See also: `aptly publish show` // @Tags Publish // @Produce json // @Param prefix path string true "publishing prefix, use `:.` instead of `.` because it is ambigious in URLs" // @Param distribution path string true "distribution name" // @Success 200 {object} deb.PublishedRepo // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution} [get] func apiPublishShow(c *gin.Context) { param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to show: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to show: %s", err)) return } c.JSON(http.StatusOK, published) } type publishedRepoCreateParams struct { // 'local' for local repositories and 'snapshot' for snapshots SourceKind string `binding:"required" json:"SourceKind" example:"snapshot"` // List of 'Component/Name' objects, 'Name' is either local repository or snapshot name Sources []sourceParams `binding:"required" json:"Sources"` // Distribution name, if missing Aptly would try to guess from sources Distribution string ` json:"Distribution" example:"bookworm"` // Value of Label: field in published repository stanza Label string ` json:"Label" example:""` // Value of Origin: field in published repository stanza Origin string ` json:"Origin" example:""` // when publishing, overwrite files in pool/ directory without notice ForceOverwrite bool ` json:"ForceOverwrite" example:"false"` // Override list of published architectures Architectures []string ` json:"Architectures" example:"amd64,armhf"` // GPG options Signing signingParams ` json:"Signing"` // Setting to yes indicates to the package manager to not install or upgrade packages from the repository without user consent NotAutomatic string ` json:"NotAutomatic" example:""` // setting to yes excludes upgrades from the NotAutomic setting ButAutomaticUpgrades string ` json:"ButAutomaticUpgrades" example:""` // Don't generate contents indexes SkipContents *bool ` json:"SkipContents" example:"false"` // Don't remove unreferenced files in prefix/component SkipCleanup *bool ` json:"SkipCleanup" example:"false"` // Skip bz2 compression for index files SkipBz2 *bool ` json:"SkipBz2" example:"false"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file. SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } // @Summary Create Published Repository // @Description **Publish a local repository or snapshot** // @Description // @Description Create a published repository. // @Description // @Description The prefix may contain a storage specifier, e.g. `s3:packages/`, or it may also be empty to publish to the root directory. // @Description // @Description **Example:** // @Description ``` // @Description $ curl -X POST -H 'Content-Type: application/json' --data '{"Distribution": "wheezy", "Sources": [{"Name": "aptly-repo"}]}' http://localhost:8080/api/publish//repos // @Description {"Architectures":["i386"],"Distribution":"wheezy","Label":"","Origin":"","Prefix":".","SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],"Storage":""} // @Description ``` // @Description // @Description See also: `aptly publish create` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body publishedRepoCreateParams true "Parameters" // @Produce json // @Success 201 {object} deb.PublishedRepo // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Source not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix} [post] func apiPublishRepoOrSnapshot(c *gin.Context) { var ( b publishedRepoCreateParams components []string names []string sources []interface{} resources []string ) param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) if c.Bind(&b) != nil { return } b.Distribution = utils.SanitizePath(b.Distribution) var archs []string for _, arch := range b.Architectures { archs = append(archs, utils.SanitizePath(arch)) } b.Architectures = archs signer, err := getSigner(&b.Signing) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to initialize GPG signer: %s", err)) return } if len(b.Sources) == 0 { AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("unable to publish: sources are empty")) return } collectionFactory := context.NewCollectionFactory() if b.SourceKind == deb.SourceSnapshot { var snapshot *deb.Snapshot snapshotCollection := collectionFactory.SnapshotCollection() for _, source := range b.Sources { components = append(components, source.Component) names = append(names, source.Name) snapshot, err = snapshotCollection.ByName(source.Name) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to publish: %s", err)) return } resources = append(resources, string(snapshot.ResourceKey())) sources = append(sources, snapshot) } } else if b.SourceKind == deb.SourceLocalRepo { var localRepo *deb.LocalRepo localCollection := collectionFactory.LocalRepoCollection() for _, source := range b.Sources { components = append(components, source.Component) names = append(names, source.Name) localRepo, err = localCollection.ByName(source.Name) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to publish: %s", err)) return } resources = append(resources, string(localRepo.Key())) sources = append(sources, localRepo) } } else { AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("unknown SourceKind")) return } multiDist := false if b.MultiDist != nil { multiDist = *b.MultiDist } collection := collectionFactory.PublishedRepoCollection() taskName := fmt.Sprintf("Publish %s repository %s/%s with components \"%s\" and sources \"%s\"", b.SourceKind, param, b.Distribution, strings.Join(components, `", "`), strings.Join(names, `", "`)) maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) { taskDetail := task.PublishDetail{ Detail: detail, } publishOutput := &task.PublishOutput{ Progress: out, PublishDetail: taskDetail, } for _, source := range sources { switch s := source.(type) { case *deb.Snapshot: snapshotCollection := collectionFactory.SnapshotCollection() err = snapshotCollection.LoadComplete(s) case *deb.LocalRepo: localCollection := collectionFactory.LocalRepoCollection() err = localCollection.LoadComplete(s) default: err = fmt.Errorf("unexpected type for source: %T", source) } if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err) } } published, err := deb.NewPublishedRepo(storage, prefix, b.Distribution, b.Architectures, components, sources, collectionFactory, multiDist) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err) } resources = append(resources, string(published.Key())) if b.Origin != "" { published.Origin = b.Origin } if b.NotAutomatic != "" { published.NotAutomatic = b.NotAutomatic } if b.ButAutomaticUpgrades != "" { published.ButAutomaticUpgrades = b.ButAutomaticUpgrades } published.Label = b.Label published.SkipContents = context.Config().SkipContentsPublishing if b.SkipContents != nil { published.SkipContents = *b.SkipContents } published.SkipBz2 = context.Config().SkipBz2Publishing if b.SkipBz2 != nil { published.SkipBz2 = *b.SkipBz2 } if b.AcquireByHash != nil { published.AcquireByHash = *b.AcquireByHash } if b.SignedBy != nil { published.SignedBy = *b.SignedBy } duplicate := collection.CheckDuplicate(published) if duplicate != nil { _ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory) return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate) } err = published.Publish(context.PackagePool(), context, collectionFactory, signer, publishOutput, b.ForceOverwrite, context.SkelPath()) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err) } err = collection.Add(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusCreated, Value: published}, nil }) } type publishedRepoUpdateSwitchParams struct { // when publishing, overwrite files in pool/ directory without notice ForceOverwrite bool ` json:"ForceOverwrite" example:"false"` // GPG options Signing signingParams ` json:"Signing"` // Don't generate contents indexes SkipContents *bool ` json:"SkipContents" example:"false"` // Skip bz2 compression for index files SkipBz2 *bool ` json:"SkipBz2" example:"false"` // Don't remove unreferenced files in prefix/component SkipCleanup *bool ` json:"SkipCleanup" example:"false"` // only when updating published snapshots, list of objects 'Component/Name' Snapshots []sourceParams ` json:"Snapshots"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } // @Summary Update Published Repository // @Description **Update a published repository** // @Description // @Description Update a published local repository or switch snapshot. // @Description // @Description For published local repositories: // @Description * update to match local repository contents // @Description // @Description For published snapshots: // @Description * switch components to new snapshot // @Description // @Description See also: `aptly publish update` / `aptly publish switch` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body publishedRepoUpdateSwitchParams true "Parameters" // @Produce json // @Success 200 {object} deb.PublishedRepo // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository or source not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution} [put] func apiPublishUpdateSwitch(c *gin.Context) { var b publishedRepoUpdateSwitchParams param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) if c.Bind(&b) != nil { return } signer, err := getSigner(&b.Signing) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to initialize GPG signer: %s", err)) return } collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() snapshotCollection := collectionFactory.SnapshotCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err)) return } if published.SourceKind == deb.SourceLocalRepo { if len(b.Snapshots) > 0 { AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("snapshots shouldn't be given when updating local repo")) return } } else if published.SourceKind == deb.SourceSnapshot { for _, snapshotInfo := range b.Snapshots { _, err2 := snapshotCollection.ByName(snapshotInfo.Name) if err2 != nil { AbortWithJSONError(c, http.StatusNotFound, err2) return } } } else { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unknown published repository type")) return } if b.SkipContents != nil { published.SkipContents = *b.SkipContents } if b.SkipBz2 != nil { published.SkipBz2 = *b.SkipBz2 } if b.AcquireByHash != nil { published.AcquireByHash = *b.AcquireByHash } if b.SignedBy != nil { published.SignedBy = *b.SignedBy } if b.MultiDist != nil { published.MultiDist = *b.MultiDist } resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.LoadComplete(published, collectionFactory) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } revision := published.ObtainRevision() sources := revision.Sources if published.SourceKind == deb.SourceSnapshot { for _, snapshotInfo := range b.Snapshots { component := snapshotInfo.Component name := snapshotInfo.Name sources[component] = name } } result, err := published.Update(collectionFactory, out) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } err = published.Publish(context.PackagePool(), context, collectionFactory, signer, out, b.ForceOverwrite, context.SkelPath()) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } if b.SkipCleanup == nil || !*b.SkipCleanup { cleanComponents := make([]string, 0, len(result.UpdatedSources)+len(result.RemovedSources)) cleanComponents = append(append(cleanComponents, result.UpdatedComponents()...), result.RemovedComponents()...) err = collection.CleanupPrefixComponentFiles(context, published, cleanComponents, collectionFactory, out) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } } return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil }) } // @Summary Delete Published Repository // @Description **Delete a published repository** // @Description // @Description Delete a distribution of a published repository and remove associated files. // @Description // @Description If no other published repositories share the same prefix, all files inside the prefix will be removed. // @Description // @Description See also: `aptly publish drop` // @Tags Publish // @Produce json // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param force query int true "force: 1 to enable" // @Param skipCleanup query int true "skipCleanup: 1 to enable" // @Param _async query bool false "Run in background and return task object" // @Success 200 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution} [delete] func apiPublishDrop(c *gin.Context) { param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) force := c.Request.URL.Query().Get("force") == "1" skipCleanup := c.Request.URL.Query().Get("SkipCleanup") == "1" collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to drop: %s", err)) return } resources := []string{string(published.Key())} taskName := fmt.Sprintf("Delete published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err := collection.Remove(context, storage, prefix, distribution, collectionFactory, out, force, skipCleanup) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %s", err) } return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, nil }) } // @Summary Add Source Component // @Description **Add a source component to a published repo** // @Description // @Description Add a component of a snapshot or local repository to be published. // @Description // @Description This call does not publish the changes, but rather schedules them for a subsequent publish update call (i.e `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source add` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body sourceParams true "Parameters" // @Produce json // @Success 201 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources [post] func apiPublishAddSource(c *gin.Context) { var b sourceParams param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to create: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to create: %s", err)) return } if c.Bind(&b) != nil { return } revision := published.ObtainRevision() sources := revision.Sources component := b.Component name := b.Name _, exists := sources[component] if exists { AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("unable to create: Component '%s' already exists", component)) return } sources[component] = name resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusCreated, Value: gin.H{}}, nil }) } // @Summary List Pending Changes // @Description **List source component changes to be applied** // @Description // @Description Return added, removed or changed components of snapshots or local repository to be published. // @Description // @Description The changes will be applied by a subsequent publish update call (i.e. `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source list` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Produce json // @Success 200 {array} []deb.SourceEntry // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository pending changes not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources [get] func apiPublishListChanges(c *gin.Context) { param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to show: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to show: %s", err)) return } revision := published.Revision if revision == nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to show: No source changes exist")) return } c.JSON(http.StatusOK, revision.SourceList()) } // @Summary Replace Source Components // @Description **Replace the source components of a published repository** // @Description // @Description Sets the components of snapshots or local repositories to be published. Existing Sourced will be replaced. // @Description // @Description This call does not publish the changes, but rather schedules them for a subsequent publish update call (i.e `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source replace` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body []sourceParams true "Parameters" // @Produce json // @Success 200 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources [put] func apiPublishSetSources(c *gin.Context) { var b []sourceParams param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err)) return } if c.Bind(&b) != nil { return } revision := published.ObtainRevision() sources := make(map[string]string, len(b)) revision.Sources = sources for _, source := range b { component := source.Component name := source.Name sources[component] = name } resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusOK, Value: revision.SourceList()}, nil }) } // @Summary Discard Pending Changes // @Description **Discard pending source component changes of a published repository** // @Description // @Description Remove all pending changes what would be applied with a subsequent publish update call (i.e. `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source drop` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param _async query bool false "Run in background and return task object" // @Produce json // @Success 200 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources [delete] func apiPublishDropChanges(c *gin.Context) { param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to delete: %s", err)) return } published.DropRevision() resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, nil }) } // @Summary Update Source Component // @Description **Update the source component of a published repository** // @Description // @Description Update a component of a snapshot or local repository to be published. // @Description // @Description This call does not publish the changes, but rather schedules them for a subsequent publish update call (i.e `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source update` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param component path string true "component name" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body sourceParams true "Parameters" // @Produce json // @Success 200 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository/component not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources/{component} [put] func apiPublishUpdateSource(c *gin.Context) { var b sourceParams param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) component := slashEscape(c.Params.ByName("component")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err)) return } revision := published.ObtainRevision() sources := revision.Sources _, exists := sources[component] if !exists { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: Component '%s' does not exist", component)) return } b.Component = component b.Name = revision.Sources[component] if c.Bind(&b) != nil { return } if b.Component != component { delete(sources, component) } component = b.Component name := b.Name sources[component] = name resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, nil }) } // @Summary Remove Source Component // @Description **Remove a source component from a published repo** // @Description // @Description Remove a source component (snapshot / local repo) from a published repository. // @Description // @Description This call does not publish the changes, but rather schedules them for a subsequent publish update call (i.e `PUT /api/publish/{prefix}/{distribution}` / `POST /api/publish/{prefix}/{distribution}/update`). // @Description // @Description See also: `aptly publish source remove` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param component path string true "component name" // @Param _async query bool false "Run in background and return task object" // @Produce json // @Success 200 // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/sources/{component} [delete] func apiPublishRemoveSource(c *gin.Context) { param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) component := slashEscape(c.Params.ByName("component")) collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to delete: %s", err)) return } revision := published.ObtainRevision() sources := revision.Sources _, exists := sources[component] if !exists { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: Component '%s' does not exist", component)) return } delete(sources, component) resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, nil }) } type publishedRepoUpdateParams struct { // when publishing, overwrite files in pool/ directory without notice ForceOverwrite bool ` json:"ForceOverwrite" example:"false"` // GPG options Signing signingParams ` json:"Signing"` // Don't generate contents indexes SkipContents *bool ` json:"SkipContents" example:"false"` // Skip bz2 compression for index files SkipBz2 *bool ` json:"SkipBz2" example:"false"` // Don't remove unreferenced files in prefix/component SkipCleanup *bool ` json:"SkipCleanup" example:"false"` // Provide index files by hash AcquireByHash *bool ` json:"AcquireByHash" example:"false"` // An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file SignedBy *string ` json:"SignedBy" example:""` // Enable multiple packages with the same filename in different distributions MultiDist *bool ` json:"MultiDist" example:"false"` } // @Summary Update Published Repository // @Description **Update a published repository** // @Description // @Description Publish pending source component changes which were added with `Add/Remove/Replace Source Components` // @Description // @Description See also: `aptly publish update` // @Tags Publish // @Param prefix path string true "publishing prefix" // @Param distribution path string true "distribution name" // @Param _async query bool false "Run in background and return task object" // @Consume json // @Param request body publishedRepoUpdateParams true "Parameters" // @Produce json // @Success 200 {object} deb.PublishedRepo // @Failure 400 {object} Error "Bad Request" // @Failure 404 {object} Error "Published repository/component not found" // @Failure 500 {object} Error "Internal Error" // @Router /api/publish/{prefix}/{distribution}/update [post] func apiPublishUpdate(c *gin.Context) { var b publishedRepoUpdateParams param := slashEscape(c.Params.ByName("prefix")) storage, prefix := deb.ParsePrefix(param) distribution := slashEscape(c.Params.ByName("distribution")) if c.Bind(&b) != nil { return } signer, err := getSigner(&b.Signing) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to initialize GPG signer: %s", err)) return } collectionFactory := context.NewCollectionFactory() collection := collectionFactory.PublishedRepoCollection() published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution) if err != nil { AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err)) return } err = collection.LoadComplete(published, collectionFactory) if err != nil { AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err)) return } if b.SkipContents != nil { published.SkipContents = *b.SkipContents } if b.SkipBz2 != nil { published.SkipBz2 = *b.SkipBz2 } if b.AcquireByHash != nil { published.AcquireByHash = *b.AcquireByHash } if b.SignedBy != nil { published.SignedBy = *b.SignedBy } if b.MultiDist != nil { published.MultiDist = *b.MultiDist } resources := []string{string(published.Key())} taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution) maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) { result, err := published.Update(collectionFactory, out) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } err = published.Publish(context.PackagePool(), context, collectionFactory, signer, out, b.ForceOverwrite, context.SkelPath()) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } err = collection.Update(published) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err) } if b.SkipCleanup == nil || !*b.SkipCleanup { cleanComponents := make([]string, 0, len(result.UpdatedSources)+len(result.RemovedSources)) cleanComponents = append(append(cleanComponents, result.UpdatedComponents()...), result.RemovedComponents()...) err = collection.CleanupPrefixComponentFiles(context, published, cleanComponents, collectionFactory, out) if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } } return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil }) }