source-code/
portofolio-backend
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
portofolio-backend/src/github/github-file-proxy.service.ts
typescript143 lines4.1 KB
import {
  Injectable,
  NotFoundException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { GithubApiService } from './github-api.service';
import { GithubSecurityService } from './github-security.service';
import {
  GithubFileResponse,
  ProxiedRepoItem,
  ProxiedFileContent,
  ProxiedReadme,
} from './interfaces/github.interfaces';

@Injectable()
export class GithubFileProxyService {
  private readonly logger = new Logger(GithubFileProxyService.name);

  constructor(
    private readonly api: GithubApiService,
    private readonly security: GithubSecurityService,
  ) {}

  /**
   * Proxies folder/directory tree queries.
   */
  async getRepoTree(
    repoName: string,
    rawPath: string,
  ): Promise<ProxiedRepoItem[]> {
    const path = this.security.validatePath(rawPath);
    const endpoint = `/repos/${this.api.username}/${repoName}/contents/${path}`;

    const res = await this.api.fetchRaw(endpoint);
    if (!res.ok) {
      if (res.status === 404) {
        throw new NotFoundException(
          `Path ${rawPath} not found in repository ${repoName}`,
        );
      }
      throw new Error(`GitHub API failed: ${res.statusText}`);
    }

    const items = (await res.json()) as unknown;
    if (!Array.isArray(items)) {
      throw new BadRequestException(
        `Requested path ${rawPath} is a file, not a directory tree.`,
      );
    }

    const typedItems = items as GithubFileResponse[];
    return typedItems.map((item) => ({
      name: item.name,
      path: item.path,
      type: item.type, // 'file' or 'dir'
      size: item.size,
    }));
  }

  /**
   * Proxies file content queries and adds metadata.
   */
  async getRepoFile(
    repoName: string,
    rawPath: string,
  ): Promise<ProxiedFileContent> {
    const path = this.security.validatePath(rawPath);
    const endpoint = `/repos/${this.api.username}/${repoName}/contents/${path}`;

    const res = await this.api.fetchRaw(endpoint);
    if (!res.ok) {
      if (res.status === 404) {
        throw new NotFoundException(
          `File ${rawPath} not found in repository ${repoName}`,
        );
      }
      throw new Error(`GitHub API failed: ${res.statusText}`);
    }

    const fileData = (await res.json()) as GithubFileResponse;
    if (fileData.type !== 'file' || !fileData.content) {
      throw new BadRequestException(`Path ${rawPath} is not a file.`);
    }

    const decodedContent = Buffer.from(fileData.content, 'base64').toString(
      'utf8',
    );
    const lines = decodedContent.split('\n').length;
    const size = fileData.size;

    // Detect language from extension
    const ext = path.split('.').pop()?.toLowerCase() || '';
    let language = 'text';
    if (ext === 'ts' || ext === 'tsx') language = 'typescript';
    else if (ext === 'js' || ext === 'jsx') language = 'javascript';
    else if (ext === 'json') language = 'json';
    else if (ext === 'md') language = 'markdown';
    else if (ext === 'html') language = 'html';
    else if (ext === 'css') language = 'css';
    else if (ext === 'py') language = 'python';
    else if (ext === 'go') language = 'go';
    else if (ext === 'rs') language = 'rust';
    else if (ext === 'sh') language = 'bash';
    else if (ext === 'yml' || ext === 'yaml') language = 'yaml';

    return {
      content: decodedContent,
      size,
      language,
      lines,
    };
  }

  /**
   * Proxies README.md file queries.
   */
  async getRepoReadme(repoName: string): Promise<ProxiedReadme> {
    const endpoint = `/repos/${this.api.username}/${repoName}/readme`;
    const res = await this.api.fetchRaw(endpoint);

    if (!res.ok) {
      if (res.status === 404) {
        throw new NotFoundException(
          `README.md not found in repository ${repoName}`,
        );
      }
      throw new Error(`GitHub API failed: ${res.statusText}`);
    }

    const fileData = (await res.json()) as GithubFileResponse;
    if (!fileData.content) {
      throw new Error('README.md content is empty or not readable.');
    }
    const decodedContent = Buffer.from(fileData.content, 'base64').toString(
      'utf8',
    );
    return {
      content: decodedContent,
    };
  }
}

About

Fullstack portfolio backend built with NestJS, Prisma, and PostgreSQL. Features JWT authentication, throttler rate limits, cache management, and automated GitHub repository synchronization.

TypeScriptNestJSPostgreSQLPrisma

Contributors

1