diff --git a/src/main/scala/gitbucket/core/api/ApiRelease.scala b/src/main/scala/gitbucket/core/api/ApiRelease.scala new file mode 100644 index 0000000..d661235 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiRelease.scala @@ -0,0 +1,42 @@ +package gitbucket.core.api +import gitbucket.core.model.{Account, ReleaseAsset, ReleaseTag} +import gitbucket.core.util.RepositoryName + +case class ApiReleaseAsset(name: String, size: Long)(asset: ReleaseAsset, repositoryName: RepositoryName) { + val label = name + val file_id = asset.fileName + val browser_download_url = ApiPath( + s"/api/v3/repos/${repositoryName.fullName}/releases/${asset.tag}/assets/${asset.fileName}" + ) +} + +object ApiReleaseAsset { + def apply(asset: ReleaseAsset, repositoryName: RepositoryName): ApiReleaseAsset = + ApiReleaseAsset(asset.label, asset.size)(asset, repositoryName) +} + +case class ApiRelease( + name: String, + tag_name: String, + body: Option[String], + author: ApiUser, + assets: Seq[ApiReleaseAsset] +) + +object ApiRelease { + def apply( + release: ReleaseTag, + assets: Seq[ReleaseAsset], + author: Account, + repositoryName: RepositoryName + ): ApiRelease = + ApiRelease( + release.name, + release.tag, + release.content, + ApiUser(author), + assets.map { asset => + ApiReleaseAsset(asset, repositoryName) + } + ) +} diff --git a/src/main/scala/gitbucket/core/api/CreateARelease.scala b/src/main/scala/gitbucket/core/api/CreateARelease.scala new file mode 100644 index 0000000..253d298 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateARelease.scala @@ -0,0 +1,10 @@ +package gitbucket.core.api + +case class CreateARelease( + tag_name: String, + target_commitish: Option[String], + name: Option[String], + body: Option[String], + draft: Option[Boolean], + prerelease: Option[Boolean] +) diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index 96fbd69..e1d0869 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -43,6 +43,8 @@ FieldSerializer[ApiCommits.Tree]() + FieldSerializer[ApiCommits.Stats]() + FieldSerializer[ApiCommits.File]() + + FieldSerializer[ApiRelease]() + + FieldSerializer[ApiReleaseAsset]() + ApiBranchProtection.enforcementLevelSerializer def apiPathSerializer(c: Context) = diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 291140a..6585af7 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -15,6 +15,7 @@ with ApiIssueLabelControllerBase with ApiOrganizationControllerBase with ApiPullRequestControllerBase + with ApiReleaseControllerBase with ApiRepositoryBranchControllerBase with ApiRepositoryCollaboratorControllerBase with ApiRepositoryCommitControllerBase @@ -31,6 +32,7 @@ with PullRequestService with CommitsService with CommitStatusService + with ReleaseService with RepositoryCreationService with IssueCreationService with HandleCommentService diff --git a/src/main/scala/gitbucket/core/controller/api/ApiReleaseControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiReleaseControllerBase.scala new file mode 100644 index 0000000..7eecf54 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/api/ApiReleaseControllerBase.scala @@ -0,0 +1,184 @@ +package gitbucket.core.controller.api +import java.io.{ByteArrayInputStream, File} + +import gitbucket.core.api._ +import gitbucket.core.controller.ControllerBase +import gitbucket.core.service.{AccountService, ReleaseService} +import gitbucket.core.util.Directory.getReleaseFilesDir +import gitbucket.core.util.{FileUtil, ReferrerAuthenticator, RepositoryName, WritableUsersAuthenticator} +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.SyntaxSugars.defining +import org.apache.commons.io.FileUtils +import org.scalatra.{Created, NoContent} + +trait ApiReleaseControllerBase extends ControllerBase { + self: AccountService with ReleaseService with ReferrerAuthenticator with WritableUsersAuthenticator => + + /** + * i. List releases for a repository + * https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository + */ + get("/api/v3/repos/:owner/:repository/releases")(referrersOnly { repository => + val releases = getReleases(repository.owner, repository.name) + JsonFormat(releases.map { rel => + val assets = getReleaseAssets(repository.owner, repository.name, rel.tag) + ApiRelease(rel, assets, getAccountByUserName(rel.author).get, RepositoryName(repository)) + }) + }) + + /** + * ii. Get a single release + * https://developer.github.com/v3/repos/releases/#get-a-single-release + * GitBucket doesn't have release id + */ + /** + * iii. Get the latest release + * https://developer.github.com/v3/repos/releases/#get-the-latest-release + */ + get("/api/v3/repos/:owner/:repository/releases/latest")(referrersOnly { repository => + getReleases(repository.owner, repository.name).lastOption + .map { release => + val assets = getReleaseAssets(repository.owner, repository.name, release.tag) + JsonFormat(ApiRelease(release, assets, getAccountByUserName(release.author).get, RepositoryName(repository))) + } + .getOrElse { + NotFound() + } + }) + + /** + * iv. Get a release by tag name + * https://developer.github.com/v3/repos/releases/#get-a-release-by-tag-name + */ + get("/api/v3/repos/:owner/:repository/releases/tags/:tag")(referrersOnly { repository => + val tag = params("tag") + getRelease(repository.owner, repository.name, tag) + .map { release => + val assets = getReleaseAssets(repository.owner, repository.name, tag) + JsonFormat(ApiRelease(release, assets, getAccountByUserName(release.author).get, RepositoryName(repository))) + } + .getOrElse { + NotFound() + } + }) + + /** + * v. Create a release + * https://developer.github.com/v3/repos/releases/#create-a-release + */ + post("/api/v3/repos/:owner/:repository/releases")(writableUsersOnly { repository => + (for { + data <- extractFromJsonBody[CreateARelease] + } yield { + createRelease( + repository.owner, + repository.name, + data.name.getOrElse(data.tag_name), + data.body, + data.tag_name, + context.loginAccount.get + ) + val release = getRelease(repository.owner, repository.name, data.tag_name).get + val assets = getReleaseAssets(repository.owner, repository.name, data.tag_name) + JsonFormat(ApiRelease(release, assets, context.loginAccount.get, RepositoryName(repository))) + }) + }) + + /** + * vi. Edit a release + * https://developer.github.com/v3/repos/releases/#edit-a-release + * Incompatiblity info: GitHub API requires :release_id, but GitBucket API requires :tag_name + */ + patch("/api/v3/repos/:owner/:repository/releases/:tag")(writableUsersOnly { repository => + (for { + data <- extractFromJsonBody[CreateARelease] + } yield { + val tag = params("tag") + updateRelease(repository.owner, repository.name, tag, data.name.getOrElse(data.tag_name), data.body) + val release = getRelease(repository.owner, repository.name, data.tag_name).get + val assets = getReleaseAssets(repository.owner, repository.name, data.tag_name) + JsonFormat(ApiRelease(release, assets, context.loginAccount.get, RepositoryName(repository))) + }) + }) + + /** + * vii. Delete a release + * https://developer.github.com/v3/repos/releases/#delete-a-release + * Incompatiblity info: GitHub API requires :release_id, but GitBucket API requires :tag_name + */ + delete("/api/v3/repos/:owner/:repository/releases/:tag")(writableUsersOnly { repository => + val tag = params("tag") + deleteRelease(repository.owner, repository.name, tag) + NoContent() + }) + + /** + * viii. List assets for a release + * https://developer.github.com/v3/repos/releases/#list-assets-for-a-release + */ + /** + * ix. Upload a release asset + * https://developer.github.com/v3/repos/releases/#upload-a-release-asset + */ + post("/api/v3/repos/:owner/:repository/releases/:tag/assets")(writableUsersOnly { repository => + val name = params("name") + val tag = params("tag") + getRelease(repository.owner, repository.name, tag) + .map { + release => + defining(FileUtil.generateFileId) { fileId => + val buf = new Array[Byte](request.inputStream.available()) + request.inputStream.read(buf) + FileUtils.writeByteArrayToFile( + new File( + getReleaseFilesDir(repository.owner, repository.name), + FileUtil.checkFilename(tag + "/" + fileId) + ), + buf + ) + createReleaseAsset( + repository.owner, + repository.name, + tag, + fileId, + name, + request.contentLength.getOrElse(0), + context.loginAccount.get + ) + getReleaseAsset(repository.owner, repository.name, tag, fileId) + .map { asset => + JsonFormat(ApiReleaseAsset(asset, RepositoryName(repository))) + } + .getOrElse { + ApiError("Unknown error") + } + } + } + .getOrElse(NotFound()) + }) + + /** + * x. Get a single release asset + * https://developer.github.com/v3/repos/releases/#get-a-single-release-asset + * Incompatibility info: GitHub requires only asset_id, but GitBucket requires tag and fileId(file_id). + */ + get("/api/v3/repos/:owner/:repository/releases/:tag/assets/:fileId")(referrersOnly { repository => + val tag = params("tag") + val fileId = params("fileId") + getReleaseAsset(repository.owner, repository.name, tag, fileId) + .map { asset => + JsonFormat(ApiReleaseAsset(asset, RepositoryName(repository))) + } + .getOrElse(NotFound()) + }) + + /* + * xi. Edit a release asset + * https://developer.github.com/v3/repos/releases/#edit-a-release-asset + */ + + /* + * xii. Delete a release asset + * https://developer.github.com/v3/repos/releases/#edit-a-release-asset + */ +} diff --git a/src/main/scala/gitbucket/core/service/ReleaseService.scala b/src/main/scala/gitbucket/core/service/ReleaseService.scala index 0a8df2f..47c7010 100644 --- a/src/main/scala/gitbucket/core/service/ReleaseService.scala +++ b/src/main/scala/gitbucket/core/service/ReleaseService.scala @@ -73,7 +73,7 @@ } def getReleases(owner: String, repository: String)(implicit s: Session): Seq[ReleaseTag] = { - ReleaseTags.filter(x => x.byRepository(owner, repository)).list + ReleaseTags.filter(x => x.byRepository(owner, repository)).sortBy(x => x.updatedDate).list } def getRelease(owner: String, repository: String, tag: String)(implicit s: Session): Option[ReleaseTag] = {