diff options
-rw-r--r-- | packages/server/.eslintrc.js | 6 | ||||
-rw-r--r-- | packages/server/src/api/git.ts | 242 | ||||
-rw-r--r-- | packages/server/src/api/git_types.d.ts | 88 | ||||
-rw-r--r-- | packages/server/src/api/util.ts | 13 | ||||
-rw-r--r-- | packages/server/src/api/v1/index.ts | 18 | ||||
-rw-r--r-- | packages/server/src/api/v1/repo.ts | 108 | ||||
-rw-r--r-- | packages/server/src/api/v1/repo/branches.ts | 40 | ||||
-rw-r--r-- | packages/server/src/api/v1/repo/index.ts | 48 | ||||
-rw-r--r-- | packages/server/src/api/v1/repo/log.ts | 40 | ||||
-rw-r--r-- | packages/server/src/app.ts | 45 | ||||
-rw-r--r-- | packages/server/src/fastify_types.ts | 13 |
11 files changed, 355 insertions, 306 deletions
diff --git a/packages/server/.eslintrc.js b/packages/server/.eslintrc.js index b9ebf8b..fb8ecfa 100644 --- a/packages/server/.eslintrc.js +++ b/packages/server/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { es2021: true, node: true }, - extends: [ "standard" ], + extends: [ "standard", "plugin:@typescript-eslint/recommended" ], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: 12 @@ -106,13 +106,13 @@ module.exports = { "max-params": [ "error", 6 ], "max-statements": "off", "max-statements-per-line": "error", - "multiline-comment-style": "error", + "multiline-comment-style": [ "error", "bare-block" ], "new-cap": "error", "new-parens": "error", "newline-per-chained-call": "error", "no-alert": "error", "no-array-constructor": "error", - "no-await-in-loop": "error", + "no-await-in-loop": "off", "no-bitwise": "error", "no-caller": "error", "no-confusing-arrow": "error", diff --git a/packages/server/src/api/git.ts b/packages/server/src/api/git.ts index 70e9fbd..d32a299 100644 --- a/packages/server/src/api/git.ts +++ b/packages/server/src/api/git.ts @@ -1,82 +1,16 @@ +import { Branch, Hunk, Hunks, LatestCommit, LogCommit, Patch, PatchHeaderData, RequestInfo, ShortBranch, ShortCommit, ShortRepository, ShortTag, ShortTreeEntry, Tree } from "./git_types"; import { Commit, ConvenientHunk, ConvenientPatch, Object, Oid, Repository, Revwalk, Tag, TreeEntry } from "nodegit"; import { FastifyReply, FastifyRequest } from "fastify"; +import { Pack, pack } from "tar-stream"; import { join, parse } from "path"; import { readFile, readdir } from "fs"; import { IncomingMessage } from "http"; +import { Route } from "../fastify_types"; import { URL } from "whatwg-url"; +import { createGzip } from "zlib"; +import { pipeline } from "stream"; import { spawn } from "child_process"; import { verifyGitRequest } from "./util"; -import { pack } from "tar-stream"; -import { pipeline } from "stream"; -import { createGzip } from "zlib"; - -interface Hunk { - new_start: number, - new_lines_cnt: number, - old_start: number, - old_lines_cnt: number, - new_lines: number[], - deleted_lines: number[], - hunk: string -} -interface Patch { - from: string, - to: string, - additions: number, - deletions: number, - too_large: boolean, - hunks: Hunk[] | null -} -export interface RequestInfo { - repo: string, - url_path: string, - parsed_url: URL, - url_path_parts: string[], - is_discovery: boolean, - service: string | null, - content_type: string -} -interface TreeEntryLatestCommit { - id: string | null, - message: string | null, - date: number | null -} -interface ShortTreeEntry { - name: string, - id: string, - type: "blob" | "tree", - latest_commit: TreeEntryLatestCommit -} -interface ShortRepository { - name: string, - description: string, - owner: string, - last_updated: number -} -type CommitAuthor = { - name: string, - email: string -} -type LogCommit = { - id: string, - author: CommitAuthor, - date: number, - message: string, - insertions: number, - deletions: number, - files_changed: number -} -type ShortCommit = { - id: string, - author: CommitAuthor, - message: string, - date: number, - patches: Patch[] -} -interface Hunks { - prev: null | number, - hunks: Hunk[] -} function addRepoDirSuffix(repo_name: string) { return repo_name.endsWith(".git") ? repo_name : `${repo_name}.git`; @@ -104,12 +38,6 @@ function getHunkContent(hunk: string[]) { } function getPatchHeaderData(patch_headers: string[], all_patches: string[]) { - interface PatchHeaderData { - indexes: number[], - lengths: number[], - last: number | null - }; - return patch_headers.reduce((patch_header_data, line, index) => { // The start of a patch header if((/^diff --git/u).test(line)) { @@ -162,11 +90,13 @@ function getPatch(patch: ConvenientPatch, too_large: boolean, hunks?: Hunk[]): P }; } -function getRequestInfo(req: FastifyRequest): RequestInfo { - const params: any = req.params; +interface Request extends FastifyRequest { + params: Route["Params"], +} - const repo = params.repo + ".git"; - const url_path = req.url.replace(params.repo, repo); +function getRequestInfo(req: Request): RequestInfo { + const repo = req.params.repo + ".git"; + const url_path = req.url.replace(req.params.repo, repo); const parsed_url = new URL(`${req.protocol}://${req.hostname}${url_path}`); const url_path_parts = parsed_url.pathname.split("/"); @@ -217,7 +147,7 @@ async function getTreeEntryLastCommit(repo: Repository, tree_entry: TreeEntry) { return result; }); - }, Promise.resolve(<TreeEntryLatestCommit>{ id: null, message: null, date: null })); + }, Promise.resolve(<LatestCommit>{ id: null, message: null, date: null })); } function readDirectory(directory: string) { @@ -232,6 +162,39 @@ function readDirectory(directory: string) { }); } +async function addArchiveEntries(entries: TreeEntry[], repo_name: string, archive: Pack) { + 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()) { + const tree = await tree_entry.getTree(); + addArchiveEntries(tree.entries(), repo_name, archive); + } + } +} + +function getTreeEntries(repo: Repository, entries: TreeEntry[]) { + return entries.reduce((acc, entry) => { + return acc.then(result => { + return getTreeEntryLastCommit(repo, entry).then(last_commit => { + result.push({ + name: parse(entry.path()).base, + id: entry.sha(), + type: entry.isBlob() ? "blob" : "tree", + latest_commit: { + id: last_commit.id, + message: last_commit.message, + date: last_commit.date + } + }); + return result; + }); + }); + }, Promise.resolve(<ShortTreeEntry[]>[])); +} + export class GitAPI { base_dir: string; @@ -239,7 +202,7 @@ export class GitAPI { this.base_dir = base_dir; } - async getLog(repo_name: string) { + async getLog(repo_name: string): Promise<LogCommit[]> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -262,7 +225,7 @@ export class GitAPI { })); } - async getRepositoryLastCommit(repo_name: string) { + async getRepositoryLastCommit(repo_name: string): Promise<number> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -271,20 +234,20 @@ export class GitAPI { return master_commit.time(); } - getRepositoryFile(repo_name: string, file: string) { - return new Promise<string>(resolve => { + getRepositoryFile(repo_name: string, file: string): Promise<string | null> { + return new Promise(resolve => { const full_repo_name = addRepoDirSuffix(repo_name); readFile(`${this.base_dir}/${full_repo_name}/${file}`, (err, content) => { - if(!err) { - resolve(content.toString().replace(/\n/gu, "")); + if(err) { + resolve(null); return; } - resolve(""); + resolve(content.toString().replace(/\n/gu, "")); }); }); } - async getRepositories() { + async getRepositories(): Promise<ShortRepository[] | null> { const dir_content = await readDirectory(this.base_dir); if(dir_content.length === 0) { @@ -323,7 +286,6 @@ export class GitAPI { const patch_start = patch_header_data.indexes[patch_index] + patch_header_data.lengths[patch_index]; const patch_end = (typeof patch_header_data.indexes[patch_index + 1] === "undefined") ? all_patches.length - 1 : patch_header_data.indexes[patch_index + 1]; const patch_content = all_patches.slice(patch_start, patch_end); - const line_lengths = patch_content.map(line => line.length).reduce((result, length) => result + length); if(patch_content.length > 5000 || line_lengths > 5000) { @@ -362,7 +324,7 @@ export class GitAPI { }; } - connectToGitHTTPBackend(req: FastifyRequest, reply: FastifyReply) { + connectToGitHTTPBackend(req: Request, reply: FastifyReply): void { const request_info = getRequestInfo(req); const valid_request = verifyGitRequest(request_info); @@ -401,58 +363,33 @@ export class GitAPI { git_pack.on("close", () => reply.raw.end()); } - async getTree(repo_name: string, tree_path: string) { + async getTree(repo_name: string, tree_path: string | null): Promise<Tree | null> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); const master_commit = await repo.getMasterCommit(); const tree = await master_commit.getTree(); - let entries = []; if(tree_path) { - try { - const path_entry = await tree.getEntry(tree_path); - - if(path_entry.isBlob()) { - return { type: "blob", content: (await path_entry.getBlob()).content().toString() }; - } + const path_entry = await tree.getEntry(tree_path) + .catch(() => null); - entries = (await path_entry.getTree()).entries(); + if(!path_entry) { + return null; } - catch(err) { - if(err.errno === -3) { - return null; - } - throw(err); + + if(path_entry.isBlob()) { + return { type: "blob", content: (await path_entry.getBlob()).content().toString() }; } - } - else { - entries = tree.entries(); + + const path_entry_tree = await path_entry.getTree(); + return { type: "tree", content: await getTreeEntries(repo, path_entry_tree.entries()) }; } - return { - type: "tree", - tree: await entries.reduce((acc, entry) => { - return acc.then(result => { - return getTreeEntryLastCommit(repo, entry).then(last_commit => { - result.push({ - name: parse(entry.path()).base, - id: entry.sha(), - type: entry.isBlob() ? "blob" : "tree", - latest_commit: { - id: last_commit.id, - message: last_commit.message, - date: last_commit.date - } - }); - return result; - }); - }); - }, Promise.resolve(<ShortTreeEntry[]>[])) - }; + return { type: "tree", content: await getTreeEntries(repo, tree.entries()) }; } - async doesObjectExist(repo_name: string, id: string) { + async doesObjectExist(repo_name: string, id: string): Promise<boolean> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -461,19 +398,19 @@ export class GitAPI { .catch(() => false); } - async doesReadmeExist(repo_name: string) { + async doesReadmeExist(repo_name: string): Promise<boolean> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); const master_commit = await repo.getMasterCommit(); const tree = await master_commit.getTree(); - const readme = await tree.getEntry("README.md").catch(() => null); - - return Boolean(readme); + return tree.getEntry("README.md").catch(() => null) + .then(() => true) + .catch(() => false); } - async getBranches(repo_name: string) { + async getBranches(repo_name: string): Promise<ShortBranch[]> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -483,11 +420,11 @@ export class GitAPI { return { id: ref.target().tostrS(), name: ref.shorthand() - } + }; }); } - async getBranch(repo_name: string, branch_id: string) { + async getBranch(repo_name: string, branch_id: string): Promise<Branch | null> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -502,6 +439,7 @@ export class GitAPI { const latest_commit = await repo.getBranchCommit(branch); return { + id: branch.target().tostrS(), name: branch.shorthand(), latest_commit: { id: latest_commit.sha(), @@ -511,7 +449,7 @@ export class GitAPI { }; } - async getTags(repo_name: string) { + async getTags(repo_name: string): Promise<ShortTag[]> { const full_repo_name = addRepoDirSuffix(repo_name); const repo = await Repository.openBare(`${this.base_dir}/${full_repo_name}`); @@ -544,16 +482,8 @@ export class GitAPI { 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 commit = await Commit.lookup(repo, (await reference.peel(Object.TYPE.COMMIT)).id()); + const tree = await commit.getTree(); const archive = pack(); const gzip = createGzip(); @@ -570,23 +500,11 @@ export class GitAPI { 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()) + addArchiveEntries(tree.entries(), repo_name, archive) .then(() => archive.finalize()) .catch(() => { archive.finalize(); reply.raw.end(); }); } -};
\ No newline at end of file +}
\ No newline at end of file diff --git a/packages/server/src/api/git_types.d.ts b/packages/server/src/api/git_types.d.ts new file mode 100644 index 0000000..ae110ff --- /dev/null +++ b/packages/server/src/api/git_types.d.ts @@ -0,0 +1,88 @@ +export interface Hunk { + new_start: number, + new_lines_cnt: number, + old_start: number, + old_lines_cnt: number, + new_lines: number[], + deleted_lines: number[], + hunk: string +} +export interface Patch { + from: string, + to: string, + additions: number, + deletions: number, + too_large: boolean, + hunks: Hunk[] | null +} +export interface RequestInfo { + repo: string, + url_path: string, + parsed_url: URL, + url_path_parts: string[], + is_discovery: boolean, + service: string | null, + content_type: string +} +export interface LatestCommit { + id: string | null, + message: string | null, + date: number | null +} +export interface ShortTreeEntry { + name: string, + id: string, + type: "blob" | "tree", + latest_commit: LatestCommit +} +export type Tree = { + type: "blob" | "tree", + content: string | ShortTreeEntry[] +} +export interface ShortRepository { + name: string, + description: string | null, + owner: string | null, + last_updated: number +} +export type Author = { + name: string, + email: string +} +export type LogCommit = { + id: string, + author: Author, + date: number, + message: string, + insertions: number, + deletions: number, + files_changed: number +} +export type ShortCommit = { + id: string, + author: Author, + message: string, + date: number, + patches: Patch[] +} +export interface Hunks { + prev: null | number, + hunks: Hunk[] +} +export interface ShortBranch { + id: string, + name: string +} +export interface Branch extends ShortBranch { + latest_commit: LatestCommit +} +export type ShortTag = { + name: string, + date: number, + author: Author +} +export type PatchHeaderData = { + indexes: number[], + lengths: number[], + last: number | null +}
\ No newline at end of file diff --git a/packages/server/src/api/util.ts b/packages/server/src/api/util.ts index 1cc135f..b05ebb7 100644 --- a/packages/server/src/api/util.ts +++ b/packages/server/src/api/util.ts @@ -1,15 +1,16 @@ -import { GitAPI, RequestInfo } from "./git"; +import { GitAPI } from "./git"; +import { RequestInfo } from "./git_types"; import { readdir } from "fs"; type VerificationResultType = "SUCCESS" | "NOT_FOUND" | "INVALID" | "ACCESS_DENIED"; export class VerificationResult { constructor(result: VerificationResultType, subject?: string) { - this.success = result === "SUCCESS" ? true : false; + this.success = result === "SUCCESS"; if(result !== "SUCCESS") { const verification_error_types = { - NOT_FOUND: { code: 404, message: `${ String(subject?.substr(0, 1).toUpperCase()) + subject?.substr(1)} not found!` }, + NOT_FOUND: { code: 404, message: `${String(subject?.substr(0, 1).toUpperCase()) + subject?.substr(1)} not found!` }, INVALID: { code: 403, message: `Invalid ${subject}` }, ACCESS_DENIED: { code: 403, message: "Access denied!" } }; @@ -24,7 +25,7 @@ export class VerificationResult { message: string | null = null; } -export function verifyRepoName(base_dir: string, repo_name: string) { +export function verifyRepoName(base_dir: string, repo_name: string): Promise<VerificationResult> { return new Promise<VerificationResult>(resolve => { console.log(repo_name); const is_valid_repo_name = (/^[a-zA-Z0-9.\-_]+$/u).test(repo_name); @@ -50,7 +51,7 @@ export function verifyRepoName(base_dir: string, repo_name: string) { }); } -export async function verifySHA(git: GitAPI, repo_name: string, sha: string) { +export async function verifySHA(git: GitAPI, repo_name: string, sha: string): Promise<VerificationResult> { if(!(/^[a-fA-F0-9]+$/u).test(sha)) { return new VerificationResult("INVALID", "sha"); } @@ -64,7 +65,7 @@ export async function verifySHA(git: GitAPI, repo_name: string, sha: string) { return new VerificationResult("SUCCESS"); } -export function verifyGitRequest(request_info: RequestInfo) { +export function verifyGitRequest(request_info: RequestInfo): VerificationResult { if((/\.\/|\.\./u).test(request_info.parsed_url.pathname)) { return new VerificationResult("INVALID", "path"); } diff --git a/packages/server/src/api/v1/index.ts b/packages/server/src/api/v1/index.ts index 31ab24f..00652a8 100644 --- a/packages/server/src/api/v1/index.ts +++ b/packages/server/src/api/v1/index.ts @@ -1,9 +1,10 @@ import { FastifyInstance, FastifyPluginOptions } from "fastify"; -import { verifyRepoName } from "../util"; import { GitAPI } from "../git"; +import { Route } from "../../fastify_types"; import repo from "./repo"; +import { verifyRepoName } from "../util"; -export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: any) { +export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: (err?: Error) => void): void { const git = new GitAPI(opts.config.settings.base_dir); fastify.setErrorHandler((err, req, reply) => { @@ -12,7 +13,7 @@ export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, do }); fastify.setNotFoundHandler((req, reply) => { reply.code(404).send({ error: "Endpoint not found!" }); - }) + }); fastify.route({ method: "GET", @@ -31,23 +32,22 @@ export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, do } }); - fastify.route({ + fastify.route<Route>({ method: "GET", url: "/repos/:repo", handler: async(req, reply) => { - const params: any = req.params; - const repo_verification = await verifyRepoName(opts.config.settings.base_dir, params.repo); + const repo_verification = await verifyRepoName(opts.config.settings.base_dir, req.params.repo); if(repo_verification.success === false && repo_verification.code) { reply.code(repo_verification.code).send({ error: repo_verification.message }); } - const desc = await git.getRepositoryFile(params.repo, "description"); + const desc = await git.getRepositoryFile(req.params.repo, "description"); - reply.send({ data: { name: params.repo, description: desc, has_readme: await git.doesReadmeExist(params.repo) } }); + reply.send({ data: { name: req.params.repo, description: desc, has_readme: await git.doesReadmeExist(req.params.repo) } }); } }); fastify.register(repo, { prefix: "/repos/:repo", config: { git: git, settings: opts.config.settings } }); done(); -};
\ No newline at end of file +}
\ No newline at end of file diff --git a/packages/server/src/api/v1/repo.ts b/packages/server/src/api/v1/repo.ts deleted file mode 100644 index 6ef2923..0000000 --- a/packages/server/src/api/v1/repo.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { FastifyInstance, FastifyPluginOptions } from "fastify"; -import { verifySHA, verifyRepoName } from "../util"; - -export default function (fastify: FastifyInstance, opts: FastifyPluginOptions, done: any) { - const git = opts.config.git; - - fastify.addHook("onRequest", async(req, reply) => { - const params: any = req.params; - const repo_verification = await verifyRepoName(opts.config.settings.base_dir, params.repo); - if(repo_verification.success === false && repo_verification.code) { - reply.code(repo_verification.code).send({ error: repo_verification.message }); - } - }); - - fastify.route({ - method: "GET", - url: "/log", - handler: async(req, reply) => { - const log = await git.getLog((<any>req.params).repo); - - if(log.length === 0) { - reply.code(500).send({ error: "Internal server error!" }); - return; - } - - reply.send({ data: log }); - } - }); - - fastify.route({ - method: "GET", - url: "/log/:commit", - handler: async(req, reply) => { - const params: any = req.params; - const commit_verification = await verifySHA(git, params.repo, params.commit); - if(commit_verification.success === false && commit_verification.code) { - reply.code(commit_verification.code).send({ error: commit_verification.message }); - } - - const commit = await git.getCommit(params.repo, params.commit); - - reply.send({ data: commit }); - } - }); - - fastify.route({ - method: "GET", - url: "/tree", - handler: async(req, reply) => { - const params: any = req.params; - const query: any = req.query; - - const tree_path = (query.length !== 0 && query.path) ? query.path : null; - - const tree = await git.getTree(params.repo, tree_path); - - if(!tree) { - reply.code(404).send({ error: "Path not found" }); - } - - reply.send({ data: tree }); - } - }); - - fastify.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.route({ - method: "GET", - url: "/branches/:branch", - handler: async(req, reply) => { - const params: any = req.params; - const branch_verification = await verifySHA(git, params.repo, params.branch); - if(branch_verification.success === false && branch_verification.code) { - reply.code(branch_verification.code).send({ error: branch_verification.message }); - } - - 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.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(); -}
\ No newline at end of file diff --git a/packages/server/src/api/v1/repo/branches.ts b/packages/server/src/api/v1/repo/branches.ts new file mode 100644 index 0000000..fd8056a --- /dev/null +++ b/packages/server/src/api/v1/repo/branches.ts @@ -0,0 +1,40 @@ +import { FastifyInstance, FastifyPluginOptions } from "fastify"; +import { GitAPI } from "../../git"; +import { Route } from "../../../fastify_types"; +import { verifySHA } from "../../util"; + +export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: (err?: Error) => void): void { + const git: GitAPI = opts.config.git; + + fastify.route<Route>({ + method: "GET", + url: "/branches", + handler: async(req, reply) => { + const branches = await git.getBranches(req.params.repo); + + reply.send({ data: branches }); + } + }); + + fastify.route<Route>({ + method: "GET", + url: "/branches/:branch", + handler: async(req, reply) => { + const branch_verification = await verifySHA(git, req.params.repo, req.params.branch); + if(branch_verification.success === false && branch_verification.code) { + reply.code(branch_verification.code).send({ error: branch_verification.message }); + } + + const branch = await git.getBranch(req.params.repo, req.params.branch); + + if(!branch) { + reply.code(404).send({ error: "Branch not found!" }); + return; + } + + reply.send({ data: branch }); + } + }); + + done(); +}
\ No newline at end of file diff --git a/packages/server/src/api/v1/repo/index.ts b/packages/server/src/api/v1/repo/index.ts new file mode 100644 index 0000000..86e5d10 --- /dev/null +++ b/packages/server/src/api/v1/repo/index.ts @@ -0,0 +1,48 @@ +import { FastifyInstance, FastifyPluginOptions, FastifyRequest } from "fastify"; +import { GitAPI } from "../../git"; +import { Route } from "../../../fastify_types"; +import branches from "./branches"; +import log from "./log"; +import { verifyRepoName } from "../../util"; + +export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: (err?: Error) => void): void { + const git: GitAPI = opts.config.git; + + fastify.addHook("onRequest", async(req: FastifyRequest<Route>, reply) => { + const repo_verification = await verifyRepoName(opts.config.settings.base_dir, req.params.repo); + if(repo_verification.success === false && repo_verification.code) { + reply.code(repo_verification.code).send({ error: repo_verification.message }); + } + }); + + fastify.register(log, { config: { git: git } }); + fastify.register(branches, { config: { git: git } }); + + fastify.route<Route>({ + method: "GET", + url: "/tree", + handler: async(req, reply) => { + const tree_path = (Object.keys(req.query).length !== 0 && req.query.path) ? req.query.path : null; + + const tree = await git.getTree(req.params.repo, tree_path); + + if(!tree) { + reply.code(404).send({ error: "Path not found" }); + return; + } + + reply.send({ data: tree }); + } + }); + + fastify.route<Route>({ + method: "GET", + url: "/tags", + handler: async(req, reply) => { + const refs = await git.getTags(req.params.repo); + reply.send({ data: refs }); + } + }); + + done(); +}
\ No newline at end of file diff --git a/packages/server/src/api/v1/repo/log.ts b/packages/server/src/api/v1/repo/log.ts new file mode 100644 index 0000000..f692b00 --- /dev/null +++ b/packages/server/src/api/v1/repo/log.ts @@ -0,0 +1,40 @@ +import { FastifyInstance, FastifyPluginOptions } from "fastify"; +import { GitAPI } from "../../git"; +import { Route } from "../../../fastify_types"; +import { verifySHA } from "../../util"; + +export default function(fastify: FastifyInstance, opts: FastifyPluginOptions, done: (err?: Error) => void): void { + const git: GitAPI = opts.config.git; + + fastify.route<Route>({ + method: "GET", + url: "/log", + handler: async(req, reply) => { + const log = await git.getLog(req.params.repo); + + if(log.length === 0) { + reply.code(500).send({ error: "Internal server error!" }); + return; + } + + reply.send({ data: log }); + } + }); + + fastify.route<Route>({ + method: "GET", + url: "/log/:commit", + handler: async(req, reply) => { + const commit_verification = await verifySHA(git, req.params.repo, req.params.commit); + if(commit_verification.success === false && commit_verification.code) { + reply.code(commit_verification.code).send({ error: commit_verification.message }); + } + + const commit = await git.getCommit(req.params.repo, req.params.commit); + + reply.send({ data: commit }); + } + }); + + done(); +}
\ No newline at end of file diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 3cb1c07..e8200d7 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,13 +1,25 @@ import { readFileSync, readdirSync } from "fs"; import { GitAPI } from "./api/git"; +import { Route } from "./fastify_types"; import api from "./api/v1"; import { exit } from "process"; import { fastify as fastifyFactory } from "fastify"; +import fastifyStatic from "fastify-static"; import { join } from "path"; import { load } from "js-yaml"; import { verifyRepoName } from "./api/util"; -const settings = <any>load(readFileSync(join(__dirname, "/../../../settings.yml"), "utf8")); +type Settings = { + host: string, + port: number, + dev_port: number, + title: string, + about: string, + base_dir: string, + production: boolean +} + +const settings = <Settings>load(readFileSync(join(__dirname, "/../../../settings.yml"), "utf8")); const settings_keys = Object.keys(settings); const mandatory_settings = [ "host", "port", "dev_port", "title", "about", "base_dir", "production" ]; @@ -57,12 +69,14 @@ fastify.setNotFoundHandler({}, function(req, reply) { }); if(settings.production) { - fastify.register(require("fastify-static"), { root: dist_dir }); + fastify.register(fastifyStatic, { root: dist_dir }); fastify.route({ method: "GET", url: "/", - handler: (req, reply: any) => reply.sendFile("index.html") + handler: (req, reply) => { + reply.sendFile("index.html"); + } }); } @@ -70,31 +84,26 @@ fastify.addContentTypeParser("application/x-git-upload-pack-request", (req, payl fastify.register(api, { prefix: "/api/v1", config: { settings: settings } }); -interface Query { - [key: string]: string -} - -fastify.route({ +fastify.route<Route>({ method: "GET", url: "/:repo([a-zA-Z0-9\\.\\-_]+)/info/refs", handler: async(req, reply) => { reply.header("Content-Type", "application/x-git-upload-pack-advertisement"); - const repo_verification = await verifyRepoName(settings.base_dir, (<any>req).params.repo); + const repo_verification = await verifyRepoName(settings.base_dir, req.params.repo); if(repo_verification.success === false && repo_verification.code) { reply.code(repo_verification.code).send(repo_verification.message); } - const query: Query = <any>req.query; - if(!query.service) { + if(!req.query.service) { reply.code(403).send("Missing service query parameter\n"); return; } - else if(query.service !== "git-upload-pack") { + else if(req.query.service !== "git-upload-pack") { reply.code(403).send("Access denied!\n"); return; } - else if(Object.keys(query).length !== 1) { + else if(Object.keys(req.query).length !== 1) { reply.code(403).send("Too many query parameters!\n"); return; } @@ -103,11 +112,12 @@ fastify.route({ } }); -fastify.route({ +fastify.route<Route>({ method: "POST", url: "/:repo([a-zA-Z0-9\\.\\-_]+)/git-upload-pack", handler: async(req, reply) => { - const repo_verification = await verifyRepoName(settings.base_dir, (<any>req).params.repo); + const repo_verification = await verifyRepoName(settings.base_dir, req.params.repo); + if(repo_verification.success === false && repo_verification.code) { reply.header("Content-Type", "application/x-git-upload-pack-result"); reply.code(repo_verification.code).send(repo_verification.message); @@ -117,12 +127,11 @@ fastify.route({ } }); -fastify.route({ +fastify.route<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); + git.downloadTagArchive(req.params.repo, req.params.tag, reply); } }); diff --git a/packages/server/src/fastify_types.ts b/packages/server/src/fastify_types.ts new file mode 100644 index 0000000..986fdf2 --- /dev/null +++ b/packages/server/src/fastify_types.ts @@ -0,0 +1,13 @@ +import { ReplyGenericInterface } from "fastify/types/reply"; +import { RequestGenericInterface } from "fastify"; + +export interface Request extends RequestGenericInterface { + Params: { + [key: string]: string + }, + Querystring: { + [key: string]: string + } +} + +export interface Route extends Request, ReplyGenericInterface {}
\ No newline at end of file |