From 2ceb6e8c986f7e77f0d74aaac065fc969d39fc3e Mon Sep 17 00:00:00 2001 From: HampusM Date: Mon, 21 Jun 2021 14:12:05 +0200 Subject: Added branch & tag API endpoints, tag tarball downloading and added handlers to the api --- packages/server/package.json | 2 + packages/server/src/api/git.ts | 126 ++++++++++++++++++++++++++++++++++++++++- packages/server/src/api/v1.ts | 44 ++++++++++++++ packages/server/src/app.ts | 9 +++ yarn.lock | 41 +++++++++++++- 5 files changed, 216 insertions(+), 6 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 90e54ea..1a9f352 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -14,12 +14,14 @@ "js-yaml": "^4.1.0", "nodegit": "^0.27.0", "nodemon": "^2.0.7", + "tar-stream": "^2.2.0", "whatwg-url": "^8.5.0" }, "devDependencies": { "@types/js-yaml": "^4.0.1", "@types/node": "^15.12.1", "@types/nodegit": "^0.27.2", + "@types/tar-stream": "^2.2.0", "@types/whatwg-url": "^8.2.0", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", diff --git a/packages/server/src/api/git.ts b/packages/server/src/api/git.ts index 880e5c0..97b0d88 100644 --- a/packages/server/src/api/git.ts +++ b/packages/server/src/api/git.ts @@ -1,4 +1,4 @@ -import { ConvenientHunk, ConvenientPatch, Repository, Revwalk, TreeEntry } from "nodegit"; +import { Commit, ConvenientHunk, ConvenientPatch, Object, Repository, Revwalk, Tag, TreeEntry } from "nodegit"; import { FastifyReply, FastifyRequest } from "fastify"; import { join, parse } from "path"; import { readFile, readdir } from "fs"; @@ -6,6 +6,9 @@ import { IncomingMessage } from "http"; import { URL } from "whatwg-url"; import { spawn } from "child_process"; import { verifyGitRequest } from "./util"; +import { pack } from "tar-stream"; +import { pipeline } from "stream"; +import { createGzip } from "zlib"; export declare namespace Git { interface Hunk { @@ -73,7 +76,7 @@ export declare namespace Git { } // eslint-disable-next-line no-unused-vars - type Commit = { + type ShortCommit = { id: string, author: string, message: string, @@ -321,7 +324,7 @@ export class GitAPI { }, Promise.resolve([])); } - async getCommit(repo_name: string, commit_oid: string): Promise { + async getCommit(repo_name: string, commit_oid: string): Promise { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); const commit = await repo.getCommit(commit_oid); @@ -484,4 +487,121 @@ export class GitAPI { return Boolean(readme); } + + async getBranches(repo_name: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + + return references.filter(ref => ref.isBranch()).map(ref => { + return { + id: ref.target().tostrS(), + name: ref.shorthand() + } + }); + } + + async getBranch(repo_name: string, branch_id: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + const branches = references.filter(ref => ref.isBranch()); + + const branch = branches.find(_branch => _branch.target().tostrS() === branch_id); + if(!branch) { + return null; + } + + const latest_commit = await repo.getBranchCommit(branch); + + return { + name: branch.shorthand(), + latest_commit: { + id: latest_commit.sha(), + message: latest_commit.message(), + date: latest_commit.time() + } + }; + } + + async getTags(repo_name: string) { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const references = await repo.getReferences(); + + return Promise.all(references.filter(ref => ref.isTag()).map(async ref => { + const tagger = (await Tag.lookup(repo, ref.target())).tagger(); + + return { + name: ref.shorthand(), + author: { + name: tagger.name(), + email: tagger.email() + }, + date: tagger.when().time() + }; + })); + } + + async downloadTagArchive(repo_name: string, tag_name: string, reply: FastifyReply): Promise { + const full_repo_name = addRepoDirSuffix(repo_name); + const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); + + const reference = await repo.getReference(tag_name) + .catch(() => { + reply.code(404).send("Tag not found!"); + return null; + }); + if(!reference) { + return; + } + + let tree; + + try { + const commit = await Commit.lookup(repo, (await reference.peel(Object.TYPE.COMMIT)).id()) + tree = await commit.getTree() + } + catch { + reply.code(500).send("Internal server error!"); + return; + } + + const archive = pack(); + const gzip = createGzip(); + + reply.raw.writeHead(200, { + "Content-Encoding": "gzip", + "Content-Type": "application/gzip", + "Content-Disposition": `attachment; filename="${repo_name}-${tag_name}.tar.gz"` + }); + + pipeline(archive, gzip, reply.raw, () => reply.raw.end()); + + gzip.on("close", () => reply.raw.end()); + gzip.on("error", () => reply.raw.end()); + archive.on("error", () => reply.raw.end()); + + async function addArchiveEntries(entries: TreeEntry[]) { + for(const tree_entry of entries) { + if(tree_entry.isBlob()) { + const blob = await tree_entry.getBlob(); + archive.entry({ name: `${repo_name}/${tree_entry.path()}` }, blob.content().toString()); + } + else if(tree_entry.isTree()) { + await addArchiveEntries((await tree_entry.getTree()).entries()); + } + } + } + + addArchiveEntries(tree.entries()) + .then(() => archive.finalize()) + .catch(() => { + archive.finalize(); + reply.raw.end(); + }); + } }; \ No newline at end of file diff --git a/packages/server/src/api/v1.ts b/packages/server/src/api/v1.ts index e6391a0..b75c473 100644 --- a/packages/server/src/api/v1.ts +++ b/packages/server/src/api/v1.ts @@ -6,6 +6,13 @@ import { GitAPI } from "./git"; export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: any) { const git = new GitAPI(opts.config.settings.base_dir); + fastify.setErrorHandler((err, req, reply) => { + reply.code(500).send({ error: "Internal server error!" }); + }); + fastify.setNotFoundHandler((req, reply) => { + reply.code(404).send({ error: "Endpoint not found!" }); + }) + fastify.route({ method: "GET", url: "/info", @@ -107,6 +114,43 @@ export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, do } }); + fastify_repo.route({ + method: "GET", + url: "/branches", + handler: async(req, reply) => { + const params: any = req.params; + const branches = await git.getBranches(params.repo); + + reply.send({ data: branches }); + } + }); + + fastify_repo.route({ + method: "GET", + url: "/branches/:branch", + handler: async(req, reply) => { + const params: any = req.params; + const branch = await git.getBranch(params.repo, params.branch); + + if(!branch) { + reply.code(404).send({ error: "Branch not found!" }); + return; + } + + reply.send({ data: branch }); + } + }); + + fastify_repo.route({ + method: "GET", + url: "/tags", + handler: async(req, reply) => { + const params: any = req.params; + const refs = await git.getTags(params.repo); + reply.send({ data: refs }); + } + }); + done_repo(); }, { prefix: "/repos/:repo" }); diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index d5f63b0..263703b 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -117,6 +117,15 @@ fastify.route({ } }); +fastify.route({ + method: "GET", + url: "/:repo([a-zA-Z0-9\\.\\-_]+)/refs/tags/:tag", + handler: (req, reply) => { + const params: any = req.params; + git.downloadTagArchive(params.repo, params.tag, reply); + } +}); + fastify.listen(settings.port, settings.host, (err: Error, addr: string) => { if(err) { console.error(err); diff --git a/yarn.lock b/yarn.lock index aff8196..564756e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1230,6 +1230,13 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4" integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ== +"@types/tar-stream@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-2.2.0.tgz#2778ef8e328a520959a39681c15c83c53553426f" + integrity sha512-sRTpT180sVigzD4SiCWJQQrqcdkWnmscWvx+cXvAoPtXbLFC5+QmKi2xwRcPe4iRu0GcVl1qTeJKUTS5hULfrw== + dependencies: + "@types/node" "*" + "@types/uglify-js@*": version "3.13.0" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124" @@ -2302,7 +2309,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.0.2: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2377,6 +2384,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.1.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -2594,6 +2610,14 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -3926,7 +3950,7 @@ encoding-negotiator@^2.0.1: resolved "https://registry.yarnpkg.com/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz#79871bb5473b81f6a0670e8de5303fb5ee0868a3" integrity sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ== -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5447,7 +5471,7 @@ icss-utils@^5.0.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.4: +ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -9412,6 +9436,17 @@ tar-stream@^1.1.2: to-buffer "^1.1.1" xtend "^4.0.0" +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^4, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" -- cgit v1.2.3-18-g5258