123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- // api/public/public.service.js
- import * as fs from "fs/promises";
- import * as path from "path";
- import axios from "axios";
- import BaseService from "../../services/base.service.js";
- class PublicService extends BaseService {
- getModel() {
- // Since we don't have a model/collection, we return null
- // This is just to satisfy the BaseService abstract method
- return null;
- }
- async getResourcesDir() {
- // Use environment variable for the resources directory
- return process.env.PUBLIC_RESOURCES_DIRECTORY || path.join(process.cwd(), 'public-resources');
- }
- isValidResourcePath(resourcePath) {
- // Prevent directory traversal and other security issues
- // This regex allows alphanumeric, underscores, hyphens, and forward slashes (for subdirectories)
- // but blocks paths with '..' which could be used for traversal
- return /^[a-zA-Z0-9_\-\/]+$/.test(resourcePath) && !resourcePath.includes('..');
- }
- async getAllResourcesInfo() {
- const resourcesDir = await this.getResourcesDir();
-
- // Ensure directory exists
- try {
- await fs.access(resourcesDir);
- } catch (error) {
- // If directory doesn't exist, create it
- if (error.code === 'ENOENT') {
- await fs.mkdir(resourcesDir, { recursive: true });
- return []; // Return empty array for newly created directory
- }
- throw error;
- }
-
- return this.processDirectory(resourcesDir);
- }
- async processDirectory(dirPath, basePath = '') {
- const result = [];
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
-
- for (const entry of entries) {
- const relativePath = path.join(basePath, entry.name);
- const fullPath = path.join(dirPath, entry.name);
-
- if (entry.isDirectory()) {
- // If it's a directory, get its stats and recursively process contents
- const stats = await fs.stat(fullPath);
- const children = await this.processDirectory(fullPath, relativePath);
-
- result.push({
- name: entry.name,
- path: relativePath,
- type: 'directory',
- size: this.calculateDirectorySize(children),
- modifiedTime: stats.mtime,
- createdTime: stats.birthtime,
- children: children
- });
- } else if (entry.isFile()) {
- // If it's a file, get its stats
- const stats = await fs.stat(fullPath);
-
- result.push({
- name: entry.name,
- path: relativePath,
- type: 'file',
- size: stats.size,
- modifiedTime: stats.mtime,
- createdTime: stats.birthtime,
- extension: path.extname(entry.name).toLowerCase()
- });
- }
- }
-
- // Sort: directories first, then files, both alphabetically
- return result.sort((a, b) => {
- if (a.type === 'directory' && b.type === 'file') return -1;
- if (a.type === 'file' && b.type === 'directory') return 1;
- return a.name.localeCompare(b.name);
- });
- }
- calculateDirectorySize(children) {
- return children.reduce((total, item) => {
- return total + (typeof item.size === 'number' ? item.size : 0);
- }, 0);
- }
- async getResourceInfo(resourceName) {
- const resourcesDir = await this.getResourcesDir();
- const resourcePath = path.join(resourcesDir, resourceName);
-
- try {
- const stats = await fs.stat(resourcePath);
-
- if (stats.isDirectory()) {
- // If it's a directory, get detailed information including contents
- const children = await this.processDirectory(resourcePath);
-
- return {
- name: path.basename(resourcePath),
- path: resourceName,
- type: 'directory',
- size: this.calculateDirectorySize(children),
- modifiedTime: stats.mtime,
- createdTime: stats.birthtime,
- children: children
- };
- } else if (stats.isFile()) {
- // If it's a file, get detailed information
- return {
- name: path.basename(resourcePath),
- path: resourceName,
- type: 'file',
- size: stats.size,
- modifiedTime: stats.mtime,
- createdTime: stats.birthtime,
- extension: path.extname(resourcePath).toLowerCase()
- };
- }
- } catch (error) {
- if (error.code === 'ENOENT') {
- // Resource not found
- return null;
- }
- throw error;
- }
-
- return null;
- }
- /**
- * Search GitHub and GitLab repositories using Google Custom Search API
- * @param {string} query - Search query
- * @param {number} page - Page number for pagination
- * @param {number} pageSize - Number of results per page
- * @returns {Object} Search results
- */
- async searchGitRepositories(query, page = 1, pageSize = 10) {
- try {
- // Make sure API key is available
- const apiKey = process.env.GOOGLE_SEARCH_API_KEY;
- const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE_ID;
-
- if (!apiKey || !searchEngineId) {
- throw new Error("Google Search API credentials are not configured properly");
- }
- // Calculate start index for pagination (Google's API uses 1-based indexing)
- const startIndex = ((page - 1) * pageSize) + 1;
-
- // Add site: operator to limit search to GitHub and GitLab
- const formattedQuery = `${query} site:github.com OR site:gitlab.com`;
-
- // Make request to Google Custom Search API
- const response = await axios.get('https://www.googleapis.com/customsearch/v1', {
- params: {
- key: apiKey,
- cx: searchEngineId,
- q: formattedQuery,
- start: startIndex,
- num: pageSize
- }
- });
-
- // Process and format the response
- const results = this.formatSearchResults(response.data);
-
- return {
- query,
- page,
- pageSize,
- totalResults: response.data.searchInformation?.totalResults || 0,
- totalPages: Math.ceil((response.data.searchInformation?.totalResults || 0) / pageSize),
- results
- };
- } catch (error) {
- // Handle API-specific errors
- if (error.response) {
- const status = error.response.status;
- const data = error.response.data;
-
- // Custom error with status code
- const customError = new Error(
- data?.error?.message || "Failed to search repositories"
- );
- customError.statusCode = status;
-
- throw customError;
- }
-
- // Re-throw other errors
- throw error;
- }
- }
- /**
- * Format search results from Google API response
- * @param {Object} apiResponse - Google API response object
- * @returns {Array} Formatted search results
- */
- formatSearchResults(apiResponse) {
- if (!apiResponse.items || !Array.isArray(apiResponse.items)) {
- return [];
- }
- return apiResponse.items.map(item => {
- // Determine if result is from GitHub or GitLab
- const isGitHub = item.link.includes('github.com');
- const isGitLab = item.link.includes('gitlab.com');
- const site = isGitHub ? 'github' : (isGitLab ? 'gitlab' : 'other');
-
- // Extract last updated info if available
- let lastUpdated = null;
- if (item.pagemap?.metatags && item.pagemap.metatags[0]) {
- lastUpdated = item.pagemap.metatags[0]['og:updated_time'] ||
- item.pagemap.metatags[0]['article:modified_time'];
- }
-
- // Extract repository owner and name if possible
- let repoOwner = null;
- let repoName = null;
-
- if (isGitHub || isGitLab) {
- const urlParts = item.link.split('/');
- // Remove empty parts and protocol
- const cleanParts = urlParts.filter(part => part && !part.includes('http'));
-
- // Format is usually: [domain.com, owner, repo, ...]
- if (cleanParts.length >= 3) {
- repoOwner = cleanParts[1];
- repoName = cleanParts[2];
- }
- }
- return {
- title: item.title,
- link: item.link,
- snippet: item.snippet,
- site,
- lastUpdated: lastUpdated ? new Date(lastUpdated).toISOString() : null,
- formattedLastUpdated: lastUpdated ? this.formatLastUpdated(lastUpdated) : 'Unknown date',
- repoOwner,
- repoName,
- cacheId: item.cacheId
- };
- });
- }
- /**
- * Format the last updated date in a human-readable format
- * @param {string} dateString - ISO date string
- * @returns {string} Formatted date string
- */
- formatLastUpdated(dateString) {
- try {
- const date = new Date(dateString);
- const now = new Date();
- const diffMs = now - date;
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
-
- if (diffDays < 1) {
- return 'Updated today';
- } else if (diffDays === 1) {
- return 'Updated yesterday';
- } else if (diffDays < 7) {
- return `Updated ${diffDays} days ago`;
- } else if (diffDays < 30) {
- const weeks = Math.floor(diffDays / 7);
- return `Updated ${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`;
- } else if (diffDays < 365) {
- const months = Math.floor(diffDays / 30);
- return `Updated ${months} ${months === 1 ? 'month' : 'months'} ago`;
- } else {
- const years = Math.floor(diffDays / 365);
- return `Updated ${years} ${years === 1 ? 'year' : 'years'} ago`;
- }
- } catch (error) {
- return 'Unknown date';
- }
- }
- }
- export default new PublicService();
|