2026-02-03 22:40:23 +00:00
|
|
|
{{define "title"}}Browse {{.PackageName}}@{{.Version}} - git-pkgs proxy{{end}}
|
|
|
|
|
|
|
|
|
|
{{define "content"}}
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
<nav class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
|
|
|
<a href="/" class="hover:text-gray-900 dark:hover:text-gray-100">Home</a>
|
|
|
|
|
<span class="mx-2">/</span>
|
|
|
|
|
<a href="/package/{{.Ecosystem}}/{{.PackageName}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.PackageName}}</a>
|
|
|
|
|
<span class="mx-2">/</span>
|
|
|
|
|
<a href="/package/{{.Ecosystem}}/{{.PackageName}}/{{.Version}}" class="hover:text-gray-900 dark:hover:text-gray-100">{{.Version}}</a>
|
|
|
|
|
<span class="mx-2">/</span>
|
|
|
|
|
<span>Browse Source</span>
|
|
|
|
|
</nav>
|
|
|
|
|
<h1 class="text-3xl font-bold">Browse Source</h1>
|
|
|
|
|
<p class="text-gray-600 dark:text-gray-400">{{.PackageName}}@{{.Version}}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid md:grid-cols-4 gap-6">
|
|
|
|
|
<!-- File Tree -->
|
|
|
|
|
<div class="md:col-span-1">
|
|
|
|
|
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 sticky top-4">
|
|
|
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800">
|
|
|
|
|
<h2 class="font-semibold">Files</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="p-2 max-h-[70vh] overflow-y-auto" id="file-tree">
|
|
|
|
|
<div class="text-center text-gray-500 dark:text-gray-400 py-4">
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- File Content -->
|
|
|
|
|
<div class="md:col-span-3">
|
|
|
|
|
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800">
|
|
|
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
|
|
|
|
|
<h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>
|
|
|
|
|
<button id="download-btn" class="hidden px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
|
|
|
Download
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="p-4" id="file-content">
|
|
|
|
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
|
|
|
|
Select a file from the tree to view its contents
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
const ecosystem = '{{.Ecosystem}}';
|
|
|
|
|
const packageName = '{{.PackageName}}';
|
|
|
|
|
const version = '{{.Version}}';
|
|
|
|
|
let currentPath = '';
|
|
|
|
|
|
2026-03-12 11:59:14 +00:00
|
|
|
// Escape a string for safe interpolation into HTML attributes and content.
|
|
|
|
|
// Prevents XSS when file paths contain quotes, angle brackets, or other special characters.
|
|
|
|
|
function escapeHTML(str) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = str;
|
|
|
|
|
return div.innerHTML.replace(/'/g, ''').replace(/"/g, '"');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 22:40:23 +00:00
|
|
|
// Load file tree for a directory
|
|
|
|
|
async function loadFileTree(path = '') {
|
|
|
|
|
try {
|
|
|
|
|
const url = `/api/browse/${ecosystem}/${packageName}/${version}?path=${encodeURIComponent(path)}`;
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
if (!response.ok) throw new Error('Failed to load directory');
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
renderFileTree(data.files, path);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading file tree:', error);
|
|
|
|
|
document.getElementById('file-tree').innerHTML =
|
|
|
|
|
'<div class="text-red-600 dark:text-red-400 p-4 text-sm">Error loading files</div>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render file tree
|
|
|
|
|
function renderFileTree(files, basePath) {
|
|
|
|
|
const container = document.getElementById('file-tree');
|
|
|
|
|
|
|
|
|
|
if (files.length === 0) {
|
|
|
|
|
container.innerHTML = '<div class="text-gray-500 dark:text-gray-400 p-4 text-sm">Empty directory</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort: directories first, then alphabetically
|
|
|
|
|
files.sort((a, b) => {
|
|
|
|
|
if (a.is_dir && !b.is_dir) return -1;
|
|
|
|
|
if (!a.is_dir && b.is_dir) return 1;
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
|
|
|
|
// Add back button if not at root
|
|
|
|
|
if (basePath) {
|
|
|
|
|
const parentPath = basePath.split('/').slice(0, -2).join('/');
|
|
|
|
|
html += `
|
|
|
|
|
<div class="px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm"
|
2026-03-12 11:59:14 +00:00
|
|
|
onclick="loadFileTree('${escapeHTML(parentPath)}'); currentPath='${escapeHTML(parentPath)}';">
|
2026-02-03 22:40:23 +00:00
|
|
|
<span class="text-gray-500 dark:text-gray-400">📁 ..</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render files and directories
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const icon = file.is_dir ? '📁' : '📄';
|
|
|
|
|
const classes = 'px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm truncate';
|
|
|
|
|
|
|
|
|
|
if (file.is_dir) {
|
|
|
|
|
html += `
|
2026-03-12 11:59:14 +00:00
|
|
|
<div class="${classes}" onclick="loadFileTree('${escapeHTML(file.path)}'); currentPath='${escapeHTML(file.path)}';">
|
|
|
|
|
<span>${icon} ${escapeHTML(file.name)}</span>
|
2026-02-03 22:40:23 +00:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
html += `
|
2026-03-12 11:59:14 +00:00
|
|
|
<div class="${classes}" onclick="loadFile('${escapeHTML(file.path)}')">
|
|
|
|
|
<span>${icon} ${escapeHTML(file.name)}</span>
|
2026-02-03 22:40:23 +00:00
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2">${formatSize(file.size)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load and display file content
|
|
|
|
|
async function loadFile(path) {
|
|
|
|
|
try {
|
|
|
|
|
const url = `/api/browse/${ecosystem}/${packageName}/${version}/file/${path}`;
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
if (!response.ok) throw new Error('Failed to load file');
|
|
|
|
|
|
|
|
|
|
const content = await response.text();
|
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
|
|
|
|
|
|
|
|
// Update path display
|
|
|
|
|
document.getElementById('file-path').textContent = path;
|
|
|
|
|
|
|
|
|
|
// Show download button
|
|
|
|
|
const downloadBtn = document.getElementById('download-btn');
|
|
|
|
|
downloadBtn.classList.remove('hidden');
|
|
|
|
|
downloadBtn.onclick = () => {
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = path.split('/').pop();
|
|
|
|
|
a.click();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render content based on type
|
|
|
|
|
const container = document.getElementById('file-content');
|
|
|
|
|
|
|
|
|
|
if (contentType && contentType.includes('image/')) {
|
2026-03-12 11:59:14 +00:00
|
|
|
container.innerHTML = `<img src="${escapeHTML(url)}" alt="${escapeHTML(path)}" class="max-w-full">`;
|
2026-02-03 22:40:23 +00:00
|
|
|
} else if (isTextContent(contentType)) {
|
|
|
|
|
// Escape HTML and display as code
|
|
|
|
|
const escaped = content
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>');
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<pre class="text-sm overflow-x-auto bg-gray-50 dark:bg-gray-950 p-4 rounded"><code>${escaped}</code></pre>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
|
|
|
|
|
<p class="mb-4">Binary file (${formatSize(content.length)})</p>
|
2026-03-12 11:59:14 +00:00
|
|
|
<button onclick="window.location.href='${escapeHTML(url)}'"
|
2026-02-03 22:40:23 +00:00
|
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
|
|
|
|
Download
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading file:', error);
|
|
|
|
|
document.getElementById('file-content').innerHTML =
|
|
|
|
|
'<div class="text-red-600 dark:text-red-400 p-4">Error loading file</div>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isTextContent(contentType) {
|
|
|
|
|
if (!contentType) return true;
|
|
|
|
|
return contentType.includes('text/') ||
|
|
|
|
|
contentType.includes('application/json') ||
|
|
|
|
|
contentType.includes('application/javascript') ||
|
|
|
|
|
contentType.includes('application/xml');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatSize(bytes) {
|
|
|
|
|
if (bytes < 1024) return bytes + ' B';
|
|
|
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
|
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load root directory on page load
|
|
|
|
|
loadFileTree();
|
|
|
|
|
</script>
|
|
|
|
|
{{end}}
|