public.service.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. // api/public/public.service.js
  2. import * as fs from "fs/promises";
  3. import * as path from "path";
  4. import axios from "axios";
  5. import BaseService from "../../services/base.service.js";
  6. class PublicService extends BaseService {
  7. getModel() {
  8. // Since we don't have a model/collection, we return null
  9. // This is just to satisfy the BaseService abstract method
  10. return null;
  11. }
  12. async getResourcesDir() {
  13. // Use environment variable for the resources directory
  14. return process.env.PUBLIC_RESOURCES_DIRECTORY || path.join(process.cwd(), 'public-resources');
  15. }
  16. isValidResourcePath(resourcePath) {
  17. // Prevent directory traversal and other security issues
  18. // This regex allows alphanumeric, underscores, hyphens, and forward slashes (for subdirectories)
  19. // but blocks paths with '..' which could be used for traversal
  20. return /^[a-zA-Z0-9_\-\/]+$/.test(resourcePath) && !resourcePath.includes('..');
  21. }
  22. async getAllResourcesInfo() {
  23. const resourcesDir = await this.getResourcesDir();
  24. // Ensure directory exists
  25. try {
  26. await fs.access(resourcesDir);
  27. } catch (error) {
  28. // If directory doesn't exist, create it
  29. if (error.code === 'ENOENT') {
  30. await fs.mkdir(resourcesDir, { recursive: true });
  31. return []; // Return empty array for newly created directory
  32. }
  33. throw error;
  34. }
  35. return this.processDirectory(resourcesDir);
  36. }
  37. async processDirectory(dirPath, basePath = '') {
  38. const result = [];
  39. const entries = await fs.readdir(dirPath, { withFileTypes: true });
  40. for (const entry of entries) {
  41. const relativePath = path.join(basePath, entry.name);
  42. const fullPath = path.join(dirPath, entry.name);
  43. if (entry.isDirectory()) {
  44. // If it's a directory, get its stats and recursively process contents
  45. const stats = await fs.stat(fullPath);
  46. const children = await this.processDirectory(fullPath, relativePath);
  47. result.push({
  48. name: entry.name,
  49. path: relativePath,
  50. type: 'directory',
  51. size: this.calculateDirectorySize(children),
  52. modifiedTime: stats.mtime,
  53. createdTime: stats.birthtime,
  54. children: children
  55. });
  56. } else if (entry.isFile()) {
  57. // If it's a file, get its stats
  58. const stats = await fs.stat(fullPath);
  59. result.push({
  60. name: entry.name,
  61. path: relativePath,
  62. type: 'file',
  63. size: stats.size,
  64. modifiedTime: stats.mtime,
  65. createdTime: stats.birthtime,
  66. extension: path.extname(entry.name).toLowerCase()
  67. });
  68. }
  69. }
  70. // Sort: directories first, then files, both alphabetically
  71. return result.sort((a, b) => {
  72. if (a.type === 'directory' && b.type === 'file') return -1;
  73. if (a.type === 'file' && b.type === 'directory') return 1;
  74. return a.name.localeCompare(b.name);
  75. });
  76. }
  77. calculateDirectorySize(children) {
  78. return children.reduce((total, item) => {
  79. return total + (typeof item.size === 'number' ? item.size : 0);
  80. }, 0);
  81. }
  82. async getResourceInfo(resourceName) {
  83. const resourcesDir = await this.getResourcesDir();
  84. const resourcePath = path.join(resourcesDir, resourceName);
  85. try {
  86. const stats = await fs.stat(resourcePath);
  87. if (stats.isDirectory()) {
  88. // If it's a directory, get detailed information including contents
  89. const children = await this.processDirectory(resourcePath);
  90. return {
  91. name: path.basename(resourcePath),
  92. path: resourceName,
  93. type: 'directory',
  94. size: this.calculateDirectorySize(children),
  95. modifiedTime: stats.mtime,
  96. createdTime: stats.birthtime,
  97. children: children
  98. };
  99. } else if (stats.isFile()) {
  100. // If it's a file, get detailed information
  101. return {
  102. name: path.basename(resourcePath),
  103. path: resourceName,
  104. type: 'file',
  105. size: stats.size,
  106. modifiedTime: stats.mtime,
  107. createdTime: stats.birthtime,
  108. extension: path.extname(resourcePath).toLowerCase()
  109. };
  110. }
  111. } catch (error) {
  112. if (error.code === 'ENOENT') {
  113. // Resource not found
  114. return null;
  115. }
  116. throw error;
  117. }
  118. return null;
  119. }
  120. /**
  121. * Search GitHub and GitLab repositories using Google Custom Search API
  122. * @param {string} query - Search query
  123. * @param {number} page - Page number for pagination
  124. * @param {number} pageSize - Number of results per page
  125. * @returns {Object} Search results
  126. */
  127. async searchGitRepositories(query, page = 1, pageSize = 10) {
  128. try {
  129. // Make sure API key is available
  130. const apiKey = process.env.GOOGLE_SEARCH_API_KEY;
  131. const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE_ID;
  132. if (!apiKey || !searchEngineId) {
  133. throw new Error("Google Search API credentials are not configured properly");
  134. }
  135. // Calculate start index for pagination (Google's API uses 1-based indexing)
  136. const startIndex = ((page - 1) * pageSize) + 1;
  137. // Add site: operator to limit search to GitHub and GitLab
  138. const formattedQuery = `${query} site:github.com OR site:gitlab.com`;
  139. // Make request to Google Custom Search API
  140. const response = await axios.get('https://www.googleapis.com/customsearch/v1', {
  141. params: {
  142. key: apiKey,
  143. cx: searchEngineId,
  144. q: formattedQuery,
  145. start: startIndex,
  146. num: pageSize
  147. }
  148. });
  149. // Process and format the response
  150. const results = this.formatSearchResults(response.data);
  151. return {
  152. query,
  153. page,
  154. pageSize,
  155. totalResults: response.data.searchInformation?.totalResults || 0,
  156. totalPages: Math.ceil((response.data.searchInformation?.totalResults || 0) / pageSize),
  157. results
  158. };
  159. } catch (error) {
  160. // Handle API-specific errors
  161. if (error.response) {
  162. const status = error.response.status;
  163. const data = error.response.data;
  164. // Custom error with status code
  165. const customError = new Error(
  166. data?.error?.message || "Failed to search repositories"
  167. );
  168. customError.statusCode = status;
  169. throw customError;
  170. }
  171. // Re-throw other errors
  172. throw error;
  173. }
  174. }
  175. /**
  176. * Format search results from Google API response
  177. * @param {Object} apiResponse - Google API response object
  178. * @returns {Array} Formatted search results
  179. */
  180. formatSearchResults(apiResponse) {
  181. if (!apiResponse.items || !Array.isArray(apiResponse.items)) {
  182. return [];
  183. }
  184. return apiResponse.items.map(item => {
  185. // Determine if result is from GitHub or GitLab
  186. const isGitHub = item.link.includes('github.com');
  187. const isGitLab = item.link.includes('gitlab.com');
  188. const site = isGitHub ? 'github' : (isGitLab ? 'gitlab' : 'other');
  189. // Extract last updated info if available
  190. let lastUpdated = null;
  191. if (item.pagemap?.metatags && item.pagemap.metatags[0]) {
  192. lastUpdated = item.pagemap.metatags[0]['og:updated_time'] ||
  193. item.pagemap.metatags[0]['article:modified_time'];
  194. }
  195. // Extract repository owner and name if possible
  196. let repoOwner = null;
  197. let repoName = null;
  198. if (isGitHub || isGitLab) {
  199. const urlParts = item.link.split('/');
  200. // Remove empty parts and protocol
  201. const cleanParts = urlParts.filter(part => part && !part.includes('http'));
  202. // Format is usually: [domain.com, owner, repo, ...]
  203. if (cleanParts.length >= 3) {
  204. repoOwner = cleanParts[1];
  205. repoName = cleanParts[2];
  206. }
  207. }
  208. return {
  209. title: item.title,
  210. link: item.link,
  211. snippet: item.snippet,
  212. site,
  213. lastUpdated: lastUpdated ? new Date(lastUpdated).toISOString() : null,
  214. formattedLastUpdated: lastUpdated ? this.formatLastUpdated(lastUpdated) : 'Unknown date',
  215. repoOwner,
  216. repoName,
  217. cacheId: item.cacheId
  218. };
  219. });
  220. }
  221. /**
  222. * Format the last updated date in a human-readable format
  223. * @param {string} dateString - ISO date string
  224. * @returns {string} Formatted date string
  225. */
  226. formatLastUpdated(dateString) {
  227. try {
  228. const date = new Date(dateString);
  229. const now = new Date();
  230. const diffMs = now - date;
  231. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  232. if (diffDays < 1) {
  233. return 'Updated today';
  234. } else if (diffDays === 1) {
  235. return 'Updated yesterday';
  236. } else if (diffDays < 7) {
  237. return `Updated ${diffDays} days ago`;
  238. } else if (diffDays < 30) {
  239. const weeks = Math.floor(diffDays / 7);
  240. return `Updated ${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`;
  241. } else if (diffDays < 365) {
  242. const months = Math.floor(diffDays / 30);
  243. return `Updated ${months} ${months === 1 ? 'month' : 'months'} ago`;
  244. } else {
  245. const years = Math.floor(diffDays / 365);
  246. return `Updated ${years} ${years === 1 ? 'year' : 'years'} ago`;
  247. }
  248. } catch (error) {
  249. return 'Unknown date';
  250. }
  251. }
  252. }
  253. export default new PublicService();