|
|
|
@ -5,7 +5,8 @@ import type { RequestHandler } from '@sveltejs/kit'; |
|
|
|
* Usage: /api/gitea-proxy/{apiPath}?baseUrl={baseUrl} |
|
|
|
* Usage: /api/gitea-proxy/{apiPath}?baseUrl={baseUrl} |
|
|
|
* Examples: |
|
|
|
* Examples: |
|
|
|
* - Gitea: /api/gitea-proxy/repos/silberengel/aitherboard/contents/README.adoc?baseUrl=https://git.imwald.eu/api/v1&ref=master |
|
|
|
* - Gitea: /api/gitea-proxy/repos/silberengel/aitherboard/contents/README.adoc?baseUrl=https://git.imwald.eu/api/v1&ref=master |
|
|
|
* - GitLab: /api/gitea-proxy/projects/owner%2Frepo/repository/files/path/raw?baseUrl=https://gitlab.com/api/v4&ref=master |
|
|
|
* - GitLab: /api/gitea-proxy/projects/owner%2Frepo/repository/files/path/to/file/raw?baseUrl=https://gitlab.com/api/v4&ref=master |
|
|
|
|
|
|
|
* (For gitlab.com, this is automatically converted to: https://gitlab.com/owner/repo/-/raw/master/path/to/file?ref_type=heads)
|
|
|
|
* - OneDev: /api/gitea-proxy/repos/owner/repo/contents/README.adoc?baseUrl=https://onedev.example.com/api/v1&ref=master |
|
|
|
* - OneDev: /api/gitea-proxy/repos/owner/repo/contents/README.adoc?baseUrl=https://onedev.example.com/api/v1&ref=master |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
@ -29,40 +30,224 @@ function buildTargetUrl(baseUrl: string, apiPath: string, searchParams: URLSearc |
|
|
|
// Ensure baseUrl doesn't have a trailing slash
|
|
|
|
// Ensure baseUrl doesn't have a trailing slash
|
|
|
|
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; |
|
|
|
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; |
|
|
|
|
|
|
|
|
|
|
|
// For GitLab, both the project path and file paths need to be re-encoded
|
|
|
|
// Handle GitLab raw file requests - convert from API v4 format to raw file URL format
|
|
|
|
// SvelteKit splits URL-encoded paths into separate segments
|
|
|
|
// GitLab raw file format: https://gitlab.com/{owner}/{repo}/-/raw/{ref}/{file_path}?ref_type=heads
|
|
|
|
|
|
|
|
// API v4 format: projects/{owner}/{repo}/repository/files/{file_path}/raw
|
|
|
|
|
|
|
|
if (apiPath.startsWith('projects/') && baseUrl.includes('gitlab.com')) { |
|
|
|
|
|
|
|
const parts = apiPath.split('/'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parts.length >= 2) { |
|
|
|
|
|
|
|
// Determine if project path is already encoded (contains %2F) or split across parts
|
|
|
|
|
|
|
|
let projectPath: string; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parts[1].includes('%2F') || parts[1].includes('%2f')) { |
|
|
|
|
|
|
|
// Project path is already encoded in parts[1] (e.g., "Pleb5%2Fjumble-fork-test")
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
projectPath = decodeURIComponent(parts[1]); |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
projectPath = parts[1]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// Remaining parts start from index 2
|
|
|
|
|
|
|
|
const remainingParts = parts.slice(2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a file path request
|
|
|
|
|
|
|
|
const filesIndex = remainingParts.indexOf('files'); |
|
|
|
|
|
|
|
if (filesIndex !== -1 && filesIndex < remainingParts.length - 1) { |
|
|
|
|
|
|
|
// This is a file path: projects/{encodedProjectPath}/repository/files/{file_path}/raw
|
|
|
|
|
|
|
|
// Extract file path segments (everything between 'files' and 'raw')
|
|
|
|
|
|
|
|
const filePathParts = remainingParts.slice(filesIndex + 1, remainingParts.length - 1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Decode any already-encoded segments
|
|
|
|
|
|
|
|
const decodedSegments = filePathParts.map(segment => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
return decodeURIComponent(segment); |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return segment; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Join with slashes to get the actual file path
|
|
|
|
|
|
|
|
const filePath = decodedSegments.join('/'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get ref from query params (default to 'master' if not provided)
|
|
|
|
|
|
|
|
const ref = searchParams.get('ref') || 'master'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract GitLab host from baseUrl (e.g., https://gitlab.com or https://git.example.com)
|
|
|
|
|
|
|
|
const gitlabHost = cleanBaseUrl.replace('/api/v4', '').replace('/api/v3', ''); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Construct GitLab raw file URL format
|
|
|
|
|
|
|
|
const rawUrl = `${gitlabHost}/${projectPath}/-/raw/${ref}/${filePath}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add ref_type=heads query parameter
|
|
|
|
|
|
|
|
const queryParts: string[] = ['ref_type=heads']; |
|
|
|
|
|
|
|
for (const [key, value] of searchParams.entries()) { |
|
|
|
|
|
|
|
if (key !== 'baseUrl' && key !== 'ref') { |
|
|
|
|
|
|
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fullUrl = `${rawUrl}${queryString}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return fullUrl; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (parts.length >= 3) { |
|
|
|
|
|
|
|
// Project path is split: parts[1] = owner, parts[2] = repo
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
projectPath = decodeURIComponent(parts[1]) + '/' + decodeURIComponent(parts[2]); |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
projectPath = `${parts[1]}/${parts[2]}`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Remaining parts start from index 3
|
|
|
|
|
|
|
|
const remainingParts = parts.slice(3); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a file path request
|
|
|
|
|
|
|
|
const filesIndex = remainingParts.indexOf('files'); |
|
|
|
|
|
|
|
if (filesIndex !== -1 && filesIndex < remainingParts.length - 1) { |
|
|
|
|
|
|
|
// This is a file path: projects/{owner}/{repo}/repository/files/{file_path}/raw
|
|
|
|
|
|
|
|
// Extract file path segments (everything between 'files' and 'raw')
|
|
|
|
|
|
|
|
const filePathParts = remainingParts.slice(filesIndex + 1, remainingParts.length - 1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Decode any already-encoded segments
|
|
|
|
|
|
|
|
const decodedSegments = filePathParts.map(segment => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
return decodeURIComponent(segment); |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return segment; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Join with slashes to get the actual file path
|
|
|
|
|
|
|
|
const filePath = decodedSegments.join('/'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get ref from query params (default to 'master' if not provided)
|
|
|
|
|
|
|
|
const ref = searchParams.get('ref') || 'master'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract GitLab host from baseUrl (e.g., https://gitlab.com or https://git.example.com)
|
|
|
|
|
|
|
|
const gitlabHost = cleanBaseUrl.replace('/api/v4', '').replace('/api/v3', ''); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Construct GitLab raw file URL format
|
|
|
|
|
|
|
|
const rawUrl = `${gitlabHost}/${projectPath}/-/raw/${ref}/${filePath}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add ref_type=heads query parameter
|
|
|
|
|
|
|
|
const queryParts: string[] = ['ref_type=heads']; |
|
|
|
|
|
|
|
for (const [key, value] of searchParams.entries()) { |
|
|
|
|
|
|
|
if (key !== 'baseUrl' && key !== 'ref') { |
|
|
|
|
|
|
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fullUrl = `${rawUrl}${queryString}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return fullUrl; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Handle GitLab API paths (non-file requests, or non-gitlab.com instances)
|
|
|
|
|
|
|
|
// GitLab format: projects/{owner}/{repo}/repository/files/{file_path}/raw
|
|
|
|
|
|
|
|
// - Project path (owner/repo) MUST be encoded as owner%2Frepo
|
|
|
|
|
|
|
|
// - File path MUST be URL-encoded with %2F for slashes (GitLab API requirement)
|
|
|
|
let processedPath = apiPath; |
|
|
|
let processedPath = apiPath; |
|
|
|
|
|
|
|
|
|
|
|
if (apiPath.startsWith('projects/')) { |
|
|
|
if (apiPath.startsWith('projects/')) { |
|
|
|
const parts = apiPath.split('/'); |
|
|
|
const parts = apiPath.split('/'); |
|
|
|
|
|
|
|
|
|
|
|
// GitLab project paths are: projects/{owner}/{repo}/...
|
|
|
|
if (parts.length >= 2) { |
|
|
|
// If we have at least 3 parts (projects, owner, repo), combine owner and repo
|
|
|
|
// Determine if project path is already encoded (contains %2F) or split across parts
|
|
|
|
if (parts.length >= 3) { |
|
|
|
let encodedProjectPath: string; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (parts[1].includes('%2F') || parts[1].includes('%2f')) { |
|
|
|
|
|
|
|
// Project path is already encoded in parts[1] (e.g., "Pleb5%2Fjumble-fork-test")
|
|
|
|
|
|
|
|
encodedProjectPath = parts[1]; |
|
|
|
|
|
|
|
// Remaining parts start from index 2
|
|
|
|
|
|
|
|
const remainingParts = parts.slice(2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a file path request
|
|
|
|
|
|
|
|
const filesIndex = remainingParts.indexOf('files'); |
|
|
|
|
|
|
|
if (filesIndex !== -1 && filesIndex < remainingParts.length - 1) { |
|
|
|
|
|
|
|
// This is a file path: projects/{encodedProjectPath}/repository/files/{file_path}/raw
|
|
|
|
|
|
|
|
// Extract file path segments (everything between 'files' and 'raw')
|
|
|
|
|
|
|
|
const filePathParts = remainingParts.slice(filesIndex + 1, remainingParts.length - 1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Decode any already-encoded segments first
|
|
|
|
|
|
|
|
const decodedSegments = filePathParts.map(segment => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
return decodeURIComponent(segment); |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return segment; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Join with slashes to get the actual file path
|
|
|
|
|
|
|
|
// GitLab API accepts file paths with actual slashes / in the URL path
|
|
|
|
|
|
|
|
// Only encode individual segments if they contain special characters, but keep slashes as /
|
|
|
|
|
|
|
|
const filePath = decodedSegments |
|
|
|
|
|
|
|
.map(segment => { |
|
|
|
|
|
|
|
// Only encode if segment contains characters that need encoding (but not slashes)
|
|
|
|
|
|
|
|
const needsEncoding = /[^a-zA-Z0-9._/-]/.test(segment); |
|
|
|
|
|
|
|
return needsEncoding ? encodeURIComponent(segment) : segment; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.join('/'); // Use actual slashes, NOT %2F
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Reconstruct: projects/{encodedProjectPath}/repository/files/{filePath}/raw
|
|
|
|
|
|
|
|
// Project path uses %2F (required), file path uses actual / (no %2F)
|
|
|
|
|
|
|
|
const beforeFiles = `projects/${encodedProjectPath}/repository/files`; |
|
|
|
|
|
|
|
const lastPart = remainingParts[remainingParts.length - 1]; // 'raw'
|
|
|
|
|
|
|
|
processedPath = `${beforeFiles}/${filePath}/${lastPart}`; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Not a file path, just reconstruct with encoded project path
|
|
|
|
|
|
|
|
processedPath = `projects/${encodedProjectPath}${remainingParts.length > 0 ? '/' + remainingParts.join('/') : ''}`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (parts.length >= 3) { |
|
|
|
|
|
|
|
// Project path is split: parts[1] = owner, parts[2] = repo
|
|
|
|
const projectPath = `${parts[1]}/${parts[2]}`; |
|
|
|
const projectPath = `${parts[1]}/${parts[2]}`; |
|
|
|
const encodedProjectPath = encodeURIComponent(projectPath); |
|
|
|
encodedProjectPath = encodeURIComponent(projectPath); // Creates owner%2Frepo
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a file path: projects/{owner}/{repo}/repository/files/{file_path}/raw
|
|
|
|
// Remaining parts start from index 3
|
|
|
|
const filesIndex = parts.indexOf('files'); |
|
|
|
const remainingParts = parts.slice(3); |
|
|
|
if (filesIndex !== -1 && filesIndex < parts.length - 1) { |
|
|
|
|
|
|
|
// Found /repository/files/, encode the file path (everything between 'files' and 'raw')
|
|
|
|
// Check if this is a file path request
|
|
|
|
const filePathParts = parts.slice(filesIndex + 1, parts.length - 1); // Exclude 'raw' at the end
|
|
|
|
const filesIndex = remainingParts.indexOf('files'); |
|
|
|
const filePath = filePathParts.join('/'); |
|
|
|
if (filesIndex !== -1 && filesIndex < remainingParts.length - 1) { |
|
|
|
// GitLab API requires the file path to be URL-encoded
|
|
|
|
// This is a file path: projects/{owner}/{repo}/repository/files/{file_path}/raw
|
|
|
|
// encodeURIComponent will encode slashes as %2F, which is what GitLab expects
|
|
|
|
// Extract file path segments (everything between 'files' and 'raw')
|
|
|
|
const encodedFilePath = encodeURIComponent(filePath); |
|
|
|
const filePathParts = remainingParts.slice(filesIndex + 1, remainingParts.length - 1); |
|
|
|
|
|
|
|
|
|
|
|
// Debug logging
|
|
|
|
// Decode any already-encoded segments first
|
|
|
|
console.log('[Gitea Proxy] File path parts:', filePathParts); |
|
|
|
const decodedSegments = filePathParts.map(segment => { |
|
|
|
console.log('[Gitea Proxy] File path (joined):', filePath); |
|
|
|
try { |
|
|
|
console.log('[Gitea Proxy] Encoded file path:', encodedFilePath); |
|
|
|
return decodeURIComponent(segment); |
|
|
|
|
|
|
|
} catch { |
|
|
|
// Reconstruct: projects/{encodedProjectPath}/repository/files/{encodedFilePath}/raw
|
|
|
|
return segment; |
|
|
|
// Rebuild the path up to 'files', then add encoded file path and 'raw'
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Join with slashes to get the actual file path
|
|
|
|
|
|
|
|
// GitLab API accepts file paths with actual slashes / in the URL path
|
|
|
|
|
|
|
|
// Only encode individual segments if they contain special characters, but keep slashes as /
|
|
|
|
|
|
|
|
const filePath = decodedSegments |
|
|
|
|
|
|
|
.map(segment => { |
|
|
|
|
|
|
|
// Only encode if segment contains characters that need encoding (but not slashes)
|
|
|
|
|
|
|
|
const needsEncoding = /[^a-zA-Z0-9._/-]/.test(segment); |
|
|
|
|
|
|
|
return needsEncoding ? encodeURIComponent(segment) : segment; |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.join('/'); // Use actual slashes, NOT %2F
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Reconstruct: projects/{encodedProjectPath}/repository/files/{filePath}/raw
|
|
|
|
|
|
|
|
// Project path uses %2F (required), file path uses actual / (no %2F)
|
|
|
|
const beforeFiles = `projects/${encodedProjectPath}/repository/files`; |
|
|
|
const beforeFiles = `projects/${encodedProjectPath}/repository/files`; |
|
|
|
processedPath = `${beforeFiles}/${encodedFilePath}/${parts[parts.length - 1]}`; |
|
|
|
const lastPart = remainingParts[remainingParts.length - 1]; // 'raw'
|
|
|
|
|
|
|
|
processedPath = `${beforeFiles}/${filePath}/${lastPart}`; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// Not a file path, just re-encode the project path
|
|
|
|
// Not a file path, just reconstruct with encoded project path
|
|
|
|
processedPath = `projects/${encodedProjectPath}${parts.length > 3 ? '/' + parts.slice(3).join('/') : ''}`; |
|
|
|
processedPath = `projects/${encodedProjectPath}${remainingParts.length > 0 ? '/' + remainingParts.join('/') : ''}`; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -79,9 +264,12 @@ function buildTargetUrl(baseUrl: string, apiPath: string, searchParams: URLSearc |
|
|
|
} |
|
|
|
} |
|
|
|
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; |
|
|
|
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; |
|
|
|
|
|
|
|
|
|
|
|
// Construct the full URL manually to preserve encoding
|
|
|
|
// Construct the full URL as a string
|
|
|
|
// Note: We construct as string because new URL() with pathname assignment would decode %2F
|
|
|
|
// We must construct as string to preserve %2F encoding
|
|
|
|
return `${cleanBaseUrl}${cleanApiPath}${queryString}`; |
|
|
|
// Using URL constructor would decode %2F, which we don't want
|
|
|
|
|
|
|
|
const fullUrl = `${cleanBaseUrl}${cleanApiPath}${queryString}`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return fullUrl; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export const GET: RequestHandler = async ({ params, url }) => { |
|
|
|
export const GET: RequestHandler = async ({ params, url }) => { |
|
|
|
@ -99,10 +287,8 @@ export const GET: RequestHandler = async ({ params, url }) => { |
|
|
|
|
|
|
|
|
|
|
|
const targetUrl = buildTargetUrl(baseUrl, apiPath, url.searchParams); |
|
|
|
const targetUrl = buildTargetUrl(baseUrl, apiPath, url.searchParams); |
|
|
|
|
|
|
|
|
|
|
|
// Debug logging (remove in production if needed)
|
|
|
|
// Use fetch with the URL string directly
|
|
|
|
console.log('[Gitea Proxy] Original path:', apiPath); |
|
|
|
// fetch() will handle the URL correctly, preserving %2F encoding
|
|
|
|
console.log('[Gitea Proxy] Target URL:', targetUrl); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(targetUrl, { |
|
|
|
const response = await fetch(targetUrl, { |
|
|
|
method: 'GET', |
|
|
|
method: 'GET', |
|
|
|
headers: { |
|
|
|
headers: { |
|
|
|
@ -114,6 +300,7 @@ export const GET: RequestHandler = async ({ params, url }) => { |
|
|
|
const contentType = response.headers.get('content-type') || 'application/json'; |
|
|
|
const contentType = response.headers.get('content-type') || 'application/json'; |
|
|
|
const body = await response.text(); |
|
|
|
const body = await response.text(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Log error responses for debugging
|
|
|
|
// Log error responses for debugging
|
|
|
|
if (!response.ok) { |
|
|
|
if (!response.ok) { |
|
|
|
console.error('[Gitea Proxy] Error response:', response.status, response.statusText); |
|
|
|
console.error('[Gitea Proxy] Error response:', response.status, response.statusText); |
|
|
|
|