feat: private/custom git repos - GitHub, GitLab, Bitbucket, custom
- Add repository URL validation for GitHub, GitLab, Bitbucket, and custom hosts - Add git provider layer (listDirectory, downloadRawFile) for all providers - Wire githubJsonService and scriptDownloader to use provider; sync/download from any supported source - Update GeneralSettingsModal placeholder and help text; .env.example and env schema for GITLAB_TOKEN, BITBUCKET_APP_PASSWORD Closes #406
This commit is contained in:
@@ -18,7 +18,12 @@ ALLOWED_SCRIPT_PATHS="scripts/"
|
|||||||
WEBSOCKET_PORT="3001"
|
WEBSOCKET_PORT="3001"
|
||||||
|
|
||||||
# User settings
|
# User settings
|
||||||
|
# Optional tokens for private repos: GITHUB_TOKEN (GitHub), GITLAB_TOKEN (GitLab),
|
||||||
|
# BITBUCKET_APP_PASSWORD or BITBUCKET_TOKEN (Bitbucket). REPO_URL and added repos
|
||||||
|
# can be GitHub, GitLab, Bitbucket, or custom Git servers.
|
||||||
GITHUB_TOKEN=
|
GITHUB_TOKEN=
|
||||||
|
GITLAB_TOKEN=
|
||||||
|
BITBUCKET_APP_PASSWORD=
|
||||||
SAVE_FILTER=false
|
SAVE_FILTER=false
|
||||||
FILTERS=
|
FILTERS=
|
||||||
AUTH_USERNAME=
|
AUTH_USERNAME=
|
||||||
|
|||||||
@@ -1617,7 +1617,7 @@ export function GeneralSettingsModal({
|
|||||||
<Input
|
<Input
|
||||||
id="new-repo-url"
|
id="new-repo-url"
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="https://github.com/owner/repo"
|
placeholder="https://github.com/owner/repo or https://git.example.com/owner/repo"
|
||||||
value={newRepoUrl}
|
value={newRepoUrl}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setNewRepoUrl(e.target.value)
|
setNewRepoUrl(e.target.value)
|
||||||
@@ -1626,8 +1626,9 @@ export function GeneralSettingsModal({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
Enter a GitHub repository URL (e.g.,
|
Supported: GitHub, GitLab, Bitbucket, or custom Git
|
||||||
https://github.com/owner/repo)
|
servers (e.g. https://github.com/owner/repo,
|
||||||
|
https://gitlab.com/owner/repo)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-border flex items-center justify-between gap-3 rounded-lg border p-3">
|
<div className="border-border flex items-center justify-between gap-3 rounded-lg border p-3">
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ export const env = createEnv({
|
|||||||
ALLOWED_SCRIPT_PATHS: z.string().default("scripts/"),
|
ALLOWED_SCRIPT_PATHS: z.string().default("scripts/"),
|
||||||
// WebSocket Configuration
|
// WebSocket Configuration
|
||||||
WEBSOCKET_PORT: z.string().default("3001"),
|
WEBSOCKET_PORT: z.string().default("3001"),
|
||||||
// GitHub Configuration
|
// Git provider tokens (optional, for private repos)
|
||||||
GITHUB_TOKEN: z.string().optional(),
|
GITHUB_TOKEN: z.string().optional(),
|
||||||
|
GITLAB_TOKEN: z.string().optional(),
|
||||||
|
BITBUCKET_APP_PASSWORD: z.string().optional(),
|
||||||
|
BITBUCKET_TOKEN: z.string().optional(),
|
||||||
// Authentication Configuration
|
// Authentication Configuration
|
||||||
AUTH_USERNAME: z.string().optional(),
|
AUTH_USERNAME: z.string().optional(),
|
||||||
AUTH_PASSWORD_HASH: z.string().optional(),
|
AUTH_PASSWORD_HASH: z.string().optional(),
|
||||||
@@ -62,8 +65,10 @@ export const env = createEnv({
|
|||||||
ALLOWED_SCRIPT_PATHS: process.env.ALLOWED_SCRIPT_PATHS,
|
ALLOWED_SCRIPT_PATHS: process.env.ALLOWED_SCRIPT_PATHS,
|
||||||
// WebSocket Configuration
|
// WebSocket Configuration
|
||||||
WEBSOCKET_PORT: process.env.WEBSOCKET_PORT,
|
WEBSOCKET_PORT: process.env.WEBSOCKET_PORT,
|
||||||
// GitHub Configuration
|
|
||||||
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
GITHUB_TOKEN: process.env.GITHUB_TOKEN,
|
||||||
|
GITLAB_TOKEN: process.env.GITLAB_TOKEN,
|
||||||
|
BITBUCKET_APP_PASSWORD: process.env.BITBUCKET_APP_PASSWORD,
|
||||||
|
BITBUCKET_TOKEN: process.env.BITBUCKET_TOKEN,
|
||||||
// Authentication Configuration
|
// Authentication Configuration
|
||||||
AUTH_USERNAME: process.env.AUTH_USERNAME,
|
AUTH_USERNAME: process.env.AUTH_USERNAME,
|
||||||
AUTH_PASSWORD_HASH: process.env.AUTH_PASSWORD_HASH,
|
AUTH_PASSWORD_HASH: process.env.AUTH_PASSWORD_HASH,
|
||||||
|
|||||||
55
src/server/lib/gitProvider/bitbucket.ts
Normal file
55
src/server/lib/gitProvider/bitbucket.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { DirEntry, GitProvider } from './types';
|
||||||
|
import { parseRepoUrl } from '../repositoryUrlValidation';
|
||||||
|
|
||||||
|
export class BitbucketProvider implements GitProvider {
|
||||||
|
async listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]> {
|
||||||
|
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const listUrl = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/src/${encodeURIComponent(branch)}/${path}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.BITBUCKET_APP_PASSWORD ?? process.env.BITBUCKET_TOKEN;
|
||||||
|
if (token) {
|
||||||
|
const auth = Buffer.from(`:${token}`).toString('base64');
|
||||||
|
headers.Authorization = `Basic ${auth}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(listUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Bitbucket API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await response.json()) as { values?: { path: string; type: string }[] };
|
||||||
|
const data = body.values ?? (Array.isArray(body) ? body : []);
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('Bitbucket API returned unexpected response');
|
||||||
|
}
|
||||||
|
return data.map((item: { path: string; type: string }) => {
|
||||||
|
const name = item.path.split('/').pop() ?? item.path;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
path: item.path,
|
||||||
|
type: item.type === 'commit_directory' ? ('dir' as const) : ('file' as const),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string> {
|
||||||
|
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const rawUrl = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/src/${encodeURIComponent(branch)}/${filePath}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.BITBUCKET_APP_PASSWORD ?? process.env.BITBUCKET_TOKEN;
|
||||||
|
if (token) {
|
||||||
|
const auth = Buffer.from(`:${token}`).toString('base64');
|
||||||
|
headers.Authorization = `Basic ${auth}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(rawUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/server/lib/gitProvider/custom.ts
Normal file
44
src/server/lib/gitProvider/custom.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { DirEntry, GitProvider } from "./types";
|
||||||
|
import { parseRepoUrl } from "../repositoryUrlValidation";
|
||||||
|
|
||||||
|
export class CustomProvider implements GitProvider {
|
||||||
|
async listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]> {
|
||||||
|
const { origin, owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const apiUrl = `${origin}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`;
|
||||||
|
const headers: Record<string, string> = { "User-Agent": "PVEScripts-Local/1.0" };
|
||||||
|
const token = process.env.GITEA_TOKEN ?? process.env.GIT_TOKEN;
|
||||||
|
if (token) headers.Authorization = `token ${token}`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Custom Git server: list directory failed (${response.status}).`);
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as { type: string; name: string; path: string }[];
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
const single = data as unknown as { type?: string; name?: string; path?: string };
|
||||||
|
if (single?.name) {
|
||||||
|
return [{ name: single.name, path: single.path ?? path, type: single.type === "dir" ? "dir" : "file" }];
|
||||||
|
}
|
||||||
|
throw new Error("Custom Git server returned unexpected response");
|
||||||
|
}
|
||||||
|
return data.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
path: item.path,
|
||||||
|
type: item.type === "dir" ? ("dir" as const) : ("file" as const),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string> {
|
||||||
|
const { origin, owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const rawUrl = `${origin}/${owner}/${repo}/raw/${encodeURIComponent(branch)}/${filePath}`;
|
||||||
|
const headers: Record<string, string> = { "User-Agent": "PVEScripts-Local/1.0" };
|
||||||
|
const token = process.env.GITEA_TOKEN ?? process.env.GIT_TOKEN;
|
||||||
|
if (token) headers.Authorization = `token ${token}`;
|
||||||
|
|
||||||
|
const response = await fetch(rawUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ${filePath} from custom Git server (${response.status}).`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/server/lib/gitProvider/github.ts
Normal file
60
src/server/lib/gitProvider/github.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { DirEntry, GitProvider } from './types';
|
||||||
|
import { parseRepoUrl } from '../repositoryUrlValidation';
|
||||||
|
|
||||||
|
export class GitHubProvider implements GitProvider {
|
||||||
|
async listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]> {
|
||||||
|
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
if (token) headers.Authorization = `token ${token}`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const err = new Error(
|
||||||
|
`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN. Status: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
(err as Error & { name: string }).name = 'RateLimitError';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { type: string; name: string; path: string }[];
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('GitHub API returned unexpected response');
|
||||||
|
}
|
||||||
|
return data.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
path: item.path,
|
||||||
|
type: item.type === 'dir' ? ('dir' as const) : ('file' as const),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string> {
|
||||||
|
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${filePath}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
if (token) headers.Authorization = `token ${token}`;
|
||||||
|
|
||||||
|
const response = await fetch(rawUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
const err = new Error(
|
||||||
|
`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN.`
|
||||||
|
);
|
||||||
|
(err as Error & { name: string }).name = 'RateLimitError';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/server/lib/gitProvider/gitlab.ts
Normal file
58
src/server/lib/gitProvider/gitlab.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { DirEntry, GitProvider } from './types';
|
||||||
|
import { parseRepoUrl } from '../repositoryUrlValidation';
|
||||||
|
|
||||||
|
export class GitLabProvider implements GitProvider {
|
||||||
|
private getBaseUrl(repoUrl: string): string {
|
||||||
|
const { origin } = parseRepoUrl(repoUrl);
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProjectId(repoUrl: string): string {
|
||||||
|
const { owner, repo } = parseRepoUrl(repoUrl);
|
||||||
|
return encodeURIComponent(`${owner}/${repo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]> {
|
||||||
|
const baseUrl = this.getBaseUrl(repoUrl);
|
||||||
|
const projectId = this.getProjectId(repoUrl);
|
||||||
|
const apiUrl = `${baseUrl}/api/v4/projects/${projectId}/repository/tree?path=${encodeURIComponent(path)}&ref=${encodeURIComponent(branch)}&per_page=100`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.GITLAB_TOKEN;
|
||||||
|
if (token) headers['PRIVATE-TOKEN'] = token;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { type: string; name: string; path: string }[];
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('GitLab API returned unexpected response');
|
||||||
|
}
|
||||||
|
return data.map((item) => ({
|
||||||
|
name: item.name,
|
||||||
|
path: item.path,
|
||||||
|
type: item.type === 'tree' ? ('dir' as const) : ('file' as const),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string> {
|
||||||
|
const baseUrl = this.getBaseUrl(repoUrl);
|
||||||
|
const projectId = this.getProjectId(repoUrl);
|
||||||
|
const encodedPath = encodeURIComponent(filePath);
|
||||||
|
const rawUrl = `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'PVEScripts-Local/1.0',
|
||||||
|
};
|
||||||
|
const token = process.env.GITLAB_TOKEN;
|
||||||
|
if (token) headers['PRIVATE-TOKEN'] = token;
|
||||||
|
|
||||||
|
const response = await fetch(rawUrl, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/server/lib/gitProvider/index.js
Normal file
1
src/server/lib/gitProvider/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { listDirectory, downloadRawFile, getRepoProvider } from "./index.ts";
|
||||||
28
src/server/lib/gitProvider/index.ts
Normal file
28
src/server/lib/gitProvider/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { DirEntry, GitProvider } from "./types";
|
||||||
|
import { getRepoProvider } from "../repositoryUrlValidation";
|
||||||
|
import { GitHubProvider } from "./github";
|
||||||
|
import { GitLabProvider } from "./gitlab";
|
||||||
|
import { BitbucketProvider } from "./bitbucket";
|
||||||
|
import { CustomProvider } from "./custom";
|
||||||
|
|
||||||
|
const providers: Record<string, GitProvider> = {
|
||||||
|
github: new GitHubProvider(),
|
||||||
|
gitlab: new GitLabProvider(),
|
||||||
|
bitbucket: new BitbucketProvider(),
|
||||||
|
custom: new CustomProvider(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { DirEntry, GitProvider };
|
||||||
|
export { getRepoProvider };
|
||||||
|
|
||||||
|
export function getGitProvider(repoUrl: string): GitProvider {
|
||||||
|
return providers[getRepoProvider(repoUrl)]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]> {
|
||||||
|
return getGitProvider(repoUrl).listDirectory(repoUrl, path, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string> {
|
||||||
|
return getGitProvider(repoUrl).downloadRawFile(repoUrl, filePath, branch);
|
||||||
|
}
|
||||||
14
src/server/lib/gitProvider/types.ts
Normal file
14
src/server/lib/gitProvider/types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Git provider interface for listing and downloading repository files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type DirEntry = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: 'file' | 'dir';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GitProvider {
|
||||||
|
listDirectory(repoUrl: string, path: string, branch: string): Promise<DirEntry[]>;
|
||||||
|
downloadRawFile(repoUrl: string, filePath: string, branch: string): Promise<string>;
|
||||||
|
}
|
||||||
37
src/server/lib/repositoryUrlValidation.js
Normal file
37
src/server/lib/repositoryUrlValidation.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Repository URL validation (JS mirror for server.js).
|
||||||
|
*/
|
||||||
|
const VALID_REPO_URL =
|
||||||
|
/^(https?:\/\/)(github\.com|gitlab\.com|bitbucket\.org|[^/]+)\/[^/]+\/[^/]+$/;
|
||||||
|
|
||||||
|
export const REPO_URL_ERROR_MESSAGE =
|
||||||
|
'Invalid repository URL. Supported: GitHub, GitLab, Bitbucket, and custom Git servers (e.g. https://host/owner/repo).';
|
||||||
|
|
||||||
|
export function isValidRepositoryUrl(url) {
|
||||||
|
if (typeof url !== 'string' || !url.trim()) return false;
|
||||||
|
return VALID_REPO_URL.test(url.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRepoProvider(url) {
|
||||||
|
if (!isValidRepositoryUrl(url)) throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
const normalized = url.trim().toLowerCase();
|
||||||
|
if (normalized.includes('github.com')) return 'github';
|
||||||
|
if (normalized.includes('gitlab.com')) return 'gitlab';
|
||||||
|
if (normalized.includes('bitbucket.org')) return 'bitbucket';
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRepoUrl(url) {
|
||||||
|
if (!isValidRepositoryUrl(url)) throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
try {
|
||||||
|
const u = new URL(url.trim());
|
||||||
|
const pathParts = u.pathname.replace(/^\/+/, '').replace(/\.git\/?$/, '').split('/');
|
||||||
|
return {
|
||||||
|
origin: u.origin,
|
||||||
|
owner: pathParts[0] ?? '',
|
||||||
|
repo: pathParts[1] ?? '',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/server/lib/repositoryUrlValidation.ts
Normal file
57
src/server/lib/repositoryUrlValidation.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Repository URL validation and provider detection.
|
||||||
|
* Supports GitHub, GitLab, Bitbucket, and custom Git servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const VALID_REPO_URL =
|
||||||
|
/^(https?:\/\/)(github\.com|gitlab\.com|bitbucket\.org|[^/]+)\/[^/]+\/[^/]+$/;
|
||||||
|
|
||||||
|
export const REPO_URL_ERROR_MESSAGE =
|
||||||
|
'Invalid repository URL. Supported: GitHub, GitLab, Bitbucket, and custom Git servers (e.g. https://host/owner/repo).';
|
||||||
|
|
||||||
|
export type RepoProvider = 'github' | 'gitlab' | 'bitbucket' | 'custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid repository URL (format only).
|
||||||
|
*/
|
||||||
|
export function isValidRepositoryUrl(url: string): boolean {
|
||||||
|
if (typeof url !== 'string' || !url.trim()) return false;
|
||||||
|
return VALID_REPO_URL.test(url.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the Git provider from a repository URL.
|
||||||
|
*/
|
||||||
|
export function getRepoProvider(url: string): RepoProvider {
|
||||||
|
if (!isValidRepositoryUrl(url)) {
|
||||||
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
const normalized = url.trim().toLowerCase();
|
||||||
|
if (normalized.includes('github.com')) return 'github';
|
||||||
|
if (normalized.includes('gitlab.com')) return 'gitlab';
|
||||||
|
if (normalized.includes('bitbucket.org')) return 'bitbucket';
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse owner and repo from a repository URL (path segments).
|
||||||
|
* Works for GitHub, GitLab, Bitbucket, and custom (host/owner/repo).
|
||||||
|
*/
|
||||||
|
export function parseRepoUrl(url: string): { origin: string; owner: string; repo: string } {
|
||||||
|
if (!isValidRepositoryUrl(url)) {
|
||||||
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const u = new URL(url.trim());
|
||||||
|
const pathParts = u.pathname.replace(/^\/+/, '').replace(/\.git\/?$/, '').split('/');
|
||||||
|
const owner = pathParts[0] ?? '';
|
||||||
|
const repo = pathParts[1] ?? '';
|
||||||
|
return {
|
||||||
|
origin: u.origin,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
|
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { repositoryService } from './repositoryService.js';
|
import { repositoryService } from './repositoryService.js';
|
||||||
|
import { listDirectory, downloadRawFile } from '../lib/gitProvider/index.js';
|
||||||
|
|
||||||
// Get environment variables
|
// Get environment variables
|
||||||
const getEnv = () => ({
|
const getEnv = () => ({
|
||||||
@@ -28,76 +29,9 @@ class GitHubJsonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl(repoUrl) {
|
|
||||||
const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
|
|
||||||
if (!urlMatch) {
|
|
||||||
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, owner, repo] = urlMatch;
|
|
||||||
return `https://api.github.com/repos/${owner}/${repo}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
extractRepoPath(repoUrl) {
|
|
||||||
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('Invalid GitHub repository URL');
|
|
||||||
}
|
|
||||||
return `${match[1]}/${match[2]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchFromGitHub(repoUrl, endpoint) {
|
|
||||||
const baseUrl = this.getBaseUrl(repoUrl);
|
|
||||||
const env = getEnv();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (env.GITHUB_TOKEN) {
|
|
||||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}${endpoint}`, { headers });
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
|
||||||
error.name = 'RateLimitError';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadJsonFile(repoUrl, filePath) {
|
async downloadJsonFile(repoUrl, filePath) {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
const repoPath = this.extractRepoPath(repoUrl);
|
const content = await downloadRawFile(repoUrl, filePath, this.branch);
|
||||||
const rawUrl = `https://raw.githubusercontent.com/${repoPath}/${this.branch}/${filePath}`;
|
|
||||||
const env = getEnv();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (env.GITHUB_TOKEN) {
|
|
||||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(rawUrl, { headers });
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits.`);
|
|
||||||
error.name = 'RateLimitError';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await response.text();
|
|
||||||
const script = JSON.parse(content);
|
const script = JSON.parse(content);
|
||||||
script.repository_url = repoUrl;
|
script.repository_url = repoUrl;
|
||||||
return script;
|
return script;
|
||||||
@@ -105,16 +39,13 @@ class GitHubJsonService {
|
|||||||
|
|
||||||
async getJsonFiles(repoUrl) {
|
async getJsonFiles(repoUrl) {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await this.fetchFromGitHub(
|
const entries = await listDirectory(repoUrl, this.jsonFolder, this.branch);
|
||||||
repoUrl,
|
return entries
|
||||||
`/contents/${this.jsonFolder}?ref=${this.branch}`
|
.filter((e) => e.type === 'file' && e.name.endsWith('.json'))
|
||||||
);
|
.map((e) => ({ name: e.name, path: e.path }));
|
||||||
|
|
||||||
return files.filter(file => file.name.endsWith('.json'));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching JSON files from GitHub (${repoUrl}):`, error);
|
console.error(`Error fetching JSON files from repository (${repoUrl}):`, error);
|
||||||
throw new Error(`Failed to fetch script files from repository: ${repoUrl}`);
|
throw new Error(`Failed to fetch script files from repository: ${repoUrl}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|||||||
import { env } from '../../env.js';
|
import { env } from '../../env.js';
|
||||||
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
||||||
import { repositoryService } from './repositoryService';
|
import { repositoryService } from './repositoryService';
|
||||||
|
import { listDirectory, downloadRawFile } from '~/server/lib/gitProvider';
|
||||||
|
|
||||||
export class GitHubJsonService {
|
export class GitHubJsonService {
|
||||||
private branch: string | null = null;
|
private branch: string | null = null;
|
||||||
@@ -22,96 +23,24 @@ export class GitHubJsonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBaseUrl(repoUrl: string): string {
|
|
||||||
const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
|
|
||||||
if (!urlMatch) {
|
|
||||||
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, owner, repo] = urlMatch;
|
|
||||||
return `https://api.github.com/repos/${owner}/${repo}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractRepoPath(repoUrl: string): string {
|
|
||||||
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('Invalid GitHub repository URL');
|
|
||||||
}
|
|
||||||
return `${match[1]}/${match[2]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchFromGitHub<T>(repoUrl: string, endpoint: string): Promise<T> {
|
|
||||||
const baseUrl = this.getBaseUrl(repoUrl);
|
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add GitHub token authentication if available
|
|
||||||
if (env.GITHUB_TOKEN) {
|
|
||||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}${endpoint}`, { headers });
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
|
||||||
error.name = 'RateLimitError';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async downloadJsonFile(repoUrl: string, filePath: string): Promise<Script> {
|
private async downloadJsonFile(repoUrl: string, filePath: string): Promise<Script> {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
const repoPath = this.extractRepoPath(repoUrl);
|
const content = await downloadRawFile(repoUrl, filePath, this.branch!);
|
||||||
const rawUrl = `https://raw.githubusercontent.com/${repoPath}/${this.branch!}/${filePath}`;
|
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add GitHub token authentication if available
|
|
||||||
if (env.GITHUB_TOKEN) {
|
|
||||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(rawUrl, { headers });
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 403) {
|
|
||||||
const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
|
||||||
error.name = 'RateLimitError';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await response.text();
|
|
||||||
const script = JSON.parse(content) as Script;
|
const script = JSON.parse(content) as Script;
|
||||||
// Add repository_url to script
|
|
||||||
script.repository_url = repoUrl;
|
script.repository_url = repoUrl;
|
||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJsonFiles(repoUrl: string): Promise<GitHubFile[]> {
|
async getJsonFiles(repoUrl: string): Promise<GitHubFile[]> {
|
||||||
this.initializeConfig();
|
this.initializeConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await this.fetchFromGitHub<GitHubFile[]>(
|
const entries = await listDirectory(repoUrl, this.jsonFolder!, this.branch!);
|
||||||
repoUrl,
|
const files: GitHubFile[] = entries
|
||||||
`/contents/${this.jsonFolder!}?ref=${this.branch!}`
|
.filter((e) => e.type === 'file' && e.name.endsWith('.json'))
|
||||||
);
|
.map((e) => ({ name: e.name, path: e.path } as GitHubFile));
|
||||||
|
return files;
|
||||||
// Filter for JSON files only
|
|
||||||
return files.filter(file => file.name.endsWith('.json'));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching JSON files from GitHub (${repoUrl}):`, error);
|
console.error(`Error fetching JSON files from repository (${repoUrl}):`, error);
|
||||||
throw new Error(`Failed to fetch script files from repository: ${repoUrl}`);
|
throw new Error(`Failed to fetch script files from repository: ${repoUrl}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,8 +162,7 @@ export class GitHubJsonService {
|
|||||||
try {
|
try {
|
||||||
console.log(`Starting JSON sync from repository: ${repoUrl}`);
|
console.log(`Starting JSON sync from repository: ${repoUrl}`);
|
||||||
|
|
||||||
// Get file list from GitHub
|
console.log(`Fetching file list from repository (${repoUrl})...`);
|
||||||
console.log(`Fetching file list from GitHub (${repoUrl})...`);
|
|
||||||
const githubFiles = await this.getJsonFiles(repoUrl);
|
const githubFiles = await this.getJsonFiles(repoUrl);
|
||||||
console.log(`Found ${githubFiles.length} JSON files in repository ${repoUrl}`);
|
console.log(`Found ${githubFiles.length} JSON files in repository ${repoUrl}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// JavaScript wrapper for repositoryService (for use with node server.js)
|
// JavaScript wrapper for repositoryService (for use with node server.js)
|
||||||
import { prisma } from '../db.js';
|
import { prisma } from '../db.js';
|
||||||
|
import { isValidRepositoryUrl, REPO_URL_ERROR_MESSAGE } from '../lib/repositoryUrlValidation.js';
|
||||||
|
|
||||||
class RepositoryService {
|
class RepositoryService {
|
||||||
/**
|
/**
|
||||||
@@ -89,9 +90,8 @@ class RepositoryService {
|
|||||||
* Create a new repository
|
* Create a new repository
|
||||||
*/
|
*/
|
||||||
async createRepository(data) {
|
async createRepository(data) {
|
||||||
// Validate GitHub URL
|
if (!isValidRepositoryUrl(data.url)) {
|
||||||
if (!data.url.match(/^https:\/\/github\.com\/[^\/]+\/[^\/]+$/)) {
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
throw new Error('Invalid GitHub repository URL. Format: https://github.com/owner/repo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
@@ -122,10 +122,9 @@ class RepositoryService {
|
|||||||
* Update repository
|
* Update repository
|
||||||
*/
|
*/
|
||||||
async updateRepository(id, data) {
|
async updateRepository(id, data) {
|
||||||
// If updating URL, validate it
|
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
if (!data.url.match(/^https:\/\/github\.com\/[^\/]+\/[^\/]+$/)) {
|
if (!isValidRepositoryUrl(data.url)) {
|
||||||
throw new Error('Invalid GitHub repository URL. Format: https://github.com/owner/repo');
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates (excluding current repo)
|
// Check for duplicates (excluding current repo)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/prefer-regexp-exec */
|
|
||||||
import { prisma } from '../db';
|
import { prisma } from '../db';
|
||||||
|
import { isValidRepositoryUrl, REPO_URL_ERROR_MESSAGE } from '../lib/repositoryUrlValidation';
|
||||||
|
|
||||||
export class RepositoryService {
|
export class RepositoryService {
|
||||||
/**
|
/**
|
||||||
@@ -93,9 +93,8 @@ export class RepositoryService {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}) {
|
}) {
|
||||||
// Validate GitHub URL
|
if (!isValidRepositoryUrl(data.url)) {
|
||||||
if (!data.url.match(/^https:\/\/github\.com\/[^\/]+\/[^\/]+$/)) {
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
throw new Error('Invalid GitHub repository URL. Format: https://github.com/owner/repo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
@@ -130,10 +129,9 @@ export class RepositoryService {
|
|||||||
url?: string;
|
url?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}) {
|
}) {
|
||||||
// If updating URL, validate it
|
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
if (!data.url.match(/^https:\/\/github\.com\/[^\/]+\/[^\/]+$/)) {
|
if (!isValidRepositoryUrl(data.url)) {
|
||||||
throw new Error('Invalid GitHub repository URL. Format: https://github.com/owner/repo');
|
throw new Error(REPO_URL_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates (excluding current repo)
|
// Check for duplicates (excluding current repo)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Real JavaScript implementation for script downloading
|
// Real JavaScript implementation for script downloading
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
|
import { writeFile, mkdir, access, readFile, unlink } from 'fs/promises';
|
||||||
|
import { downloadRawFile } from '../lib/gitProvider/index.js';
|
||||||
|
|
||||||
export class ScriptDownloaderService {
|
export class ScriptDownloaderService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -82,51 +83,18 @@ export class ScriptDownloaderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract repository path from GitHub URL
|
* Download a file from the repository (GitHub, GitLab, Bitbucket, or custom)
|
||||||
* @param {string} repoUrl - The GitHub repository URL
|
* @param {string} repoUrl - The repository URL
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
extractRepoPath(repoUrl) {
|
|
||||||
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(repoUrl);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
|
|
||||||
}
|
|
||||||
return `${match[1]}/${match[2]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a file from GitHub
|
|
||||||
* @param {string} repoUrl - The GitHub repository URL
|
|
||||||
* @param {string} filePath - The file path within the repository
|
* @param {string} filePath - The file path within the repository
|
||||||
* @param {string} [branch] - The branch to download from
|
* @param {string} [branch] - The branch to download from
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async downloadFileFromGitHub(repoUrl, filePath, branch = 'main') {
|
async downloadFileFromRepo(repoUrl, filePath, branch = 'main') {
|
||||||
this.initializeConfig();
|
|
||||||
if (!repoUrl) {
|
if (!repoUrl) {
|
||||||
throw new Error('Repository URL is not set');
|
throw new Error('Repository URL is not set');
|
||||||
}
|
}
|
||||||
|
console.log(`Downloading from repository: ${repoUrl} (${filePath})`);
|
||||||
const repoPath = this.extractRepoPath(repoUrl);
|
return downloadRawFile(repoUrl, filePath, branch);
|
||||||
const url = `https://raw.githubusercontent.com/${repoPath}/${branch}/${filePath}`;
|
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'PVEScripts-Local/1.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add GitHub token authentication if available
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
|
||||||
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Downloading from GitHub: ${url}`);
|
|
||||||
const response = await fetch(url, { headers });
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download ${filePath} from ${repoUrl}: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.text();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,9 +152,8 @@ export class ScriptDownloaderService {
|
|||||||
const fileName = scriptPath.split('/').pop();
|
const fileName = scriptPath.split('/').pop();
|
||||||
|
|
||||||
if (fileName) {
|
if (fileName) {
|
||||||
// Download from GitHub using the script's repository URL
|
|
||||||
console.log(`Downloading script file: ${scriptPath} from ${repoUrl}`);
|
console.log(`Downloading script file: ${scriptPath} from ${repoUrl}`);
|
||||||
const content = await this.downloadFileFromGitHub(repoUrl, scriptPath, branch);
|
const content = await this.downloadFileFromRepo(repoUrl, scriptPath, branch);
|
||||||
|
|
||||||
// Determine target directory based on script path
|
// Determine target directory based on script path
|
||||||
let targetDir;
|
let targetDir;
|
||||||
@@ -250,7 +217,7 @@ export class ScriptDownloaderService {
|
|||||||
const installScriptName = `${script.slug}-install.sh`;
|
const installScriptName = `${script.slug}-install.sh`;
|
||||||
try {
|
try {
|
||||||
console.log(`Downloading install script: install/${installScriptName} from ${repoUrl}`);
|
console.log(`Downloading install script: install/${installScriptName} from ${repoUrl}`);
|
||||||
const installContent = await this.downloadFileFromGitHub(repoUrl, `install/${installScriptName}`, branch);
|
const installContent = await this.downloadFileFromRepo(repoUrl, `install/${installScriptName}`, branch);
|
||||||
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
|
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||||
await writeFile(localInstallPath, installContent, 'utf-8');
|
await writeFile(localInstallPath, installContent, 'utf-8');
|
||||||
files.push(`install/${installScriptName}`);
|
files.push(`install/${installScriptName}`);
|
||||||
@@ -274,7 +241,7 @@ export class ScriptDownloaderService {
|
|||||||
const alpineInstallScriptName = `alpine-${script.slug}-install.sh`;
|
const alpineInstallScriptName = `alpine-${script.slug}-install.sh`;
|
||||||
try {
|
try {
|
||||||
console.log(`[${script.slug}] Downloading alpine install script: install/${alpineInstallScriptName} from ${repoUrl}`);
|
console.log(`[${script.slug}] Downloading alpine install script: install/${alpineInstallScriptName} from ${repoUrl}`);
|
||||||
const alpineInstallContent = await this.downloadFileFromGitHub(repoUrl, `install/${alpineInstallScriptName}`, branch);
|
const alpineInstallContent = await this.downloadFileFromRepo(repoUrl, `install/${alpineInstallScriptName}`, branch);
|
||||||
const localAlpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);
|
const localAlpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName);
|
||||||
await writeFile(localAlpineInstallPath, alpineInstallContent, 'utf-8');
|
await writeFile(localAlpineInstallPath, alpineInstallContent, 'utf-8');
|
||||||
files.push(`install/${alpineInstallScriptName}`);
|
files.push(`install/${alpineInstallScriptName}`);
|
||||||
@@ -681,7 +648,7 @@ export class ScriptDownloaderService {
|
|||||||
console.log(`[Comparison] Local file size: ${localContent.length} bytes`);
|
console.log(`[Comparison] Local file size: ${localContent.length} bytes`);
|
||||||
|
|
||||||
// Download remote content from the script's repository
|
// Download remote content from the script's repository
|
||||||
const remoteContent = await this.downloadFileFromGitHub(repoUrl, remotePath, branch);
|
const remoteContent = await this.downloadFileFromRepo(repoUrl, remotePath, branch);
|
||||||
console.log(`[Comparison] Remote file size: ${remoteContent.length} bytes`);
|
console.log(`[Comparison] Remote file size: ${remoteContent.length} bytes`);
|
||||||
|
|
||||||
// Apply modification only for CT scripts, not for other script types
|
// Apply modification only for CT scripts, not for other script types
|
||||||
@@ -739,7 +706,7 @@ export class ScriptDownloaderService {
|
|||||||
// Find the corresponding script path in install_methods
|
// Find the corresponding script path in install_methods
|
||||||
const method = script.install_methods?.find(m => m.script === filePath);
|
const method = script.install_methods?.find(m => m.script === filePath);
|
||||||
if (method?.script) {
|
if (method?.script) {
|
||||||
const downloadedContent = await this.downloadFileFromGitHub(repoUrl, method.script, branch);
|
const downloadedContent = await this.downloadFileFromRepo(repoUrl, method.script, branch);
|
||||||
remoteContent = this.modifyScriptContent(downloadedContent);
|
remoteContent = this.modifyScriptContent(downloadedContent);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -756,7 +723,7 @@ export class ScriptDownloaderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
remoteContent = await this.downloadFileFromGitHub(repoUrl, filePath, branch);
|
remoteContent = await this.downloadFileFromRepo(repoUrl, filePath, branch);
|
||||||
} catch {
|
} catch {
|
||||||
// Error downloading remote install script
|
// Error downloading remote install script
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user