This commit is contained in:
jarek committed 2026-01-17 15:06:14 +01:00
1 parent 62636426bf
commit c185d00dc3
60 files changed
+2830 -918

No files matched your search

+32 -12
View File
@@ -1,10 +1,14 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getRegistry } from '$lib/server/db';
import { getRegistryAuth } from '$lib/server/docker';
const PAGE_SIZE = 100;
export const GET: RequestHandler = async ({ url }) => {
try {
const registryId = url.searchParams.get('registry');
const lastParam = url.searchParams.get('last'); // For pagination
if (!registryId) {
return json({ error: 'Registry ID is required' }, { status: 400 });
@@ -20,22 +24,20 @@ export const GET: RequestHandler = async ({ url }) => {
return json({ error: 'Docker Hub does not support catalog listing. Please use search instead.' }, { status: 400 });
}
// Build the catalog URL
let catalogUrl = registry.url;
if (!catalogUrl.endsWith('/')) {
catalogUrl += '/';
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
// Build catalog URL with pagination
let catalogUrl = `${baseUrl}/v2/_catalog?n=${PAGE_SIZE}`;
if (lastParam) {
catalogUrl += `&last=${encodeURIComponent(lastParam)}`;
}
catalogUrl += 'v2/_catalog';
// Prepare headers
const headers: HeadersInit = {
'Accept': 'application/json'
};
// Add auth if credentials are present
if (registry.username && registry.password) {
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
if (authHeader) {
headers['Authorization'] = authHeader;
}
const response = await fetch(catalogUrl, {
@@ -56,7 +58,18 @@ export const GET: RequestHandler = async ({ url }) => {
const data = await response.json();
// The V2 API returns { repositories: [...] }
const repositories = data.repositories || [];
const repositories: string[] = data.repositories || [];
// Parse Link header for pagination
// Format: </v2/_catalog?last=xxx&n=100>; rel="next"
let nextLast: string | null = null;
const linkHeader = response.headers.get('Link');
if (linkHeader) {
const nextMatch = linkHeader.match(/<[^>]*[?&]last=([^&>]+)[^>]*>;\s*rel="next"/);
if (nextMatch) {
nextLast = decodeURIComponent(nextMatch[1]);
}
}
// For each repository, we could fetch tags, but that's expensive
// Just return the repository names for now
@@ -68,7 +81,14 @@ export const GET: RequestHandler = async ({ url }) => {
is_automated: false
}));
return json(results);
return json({
repositories: results,
pagination: {
pageSize: PAGE_SIZE,
hasMore: !!nextLast,
nextLast: nextLast
}
});
} catch (error: any) {
console.error('Error fetching registry catalog:', error);
+6 -9
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getRegistry } from '$lib/server/db';
import { getRegistryAuth } from '$lib/server/docker';
function isDockerHub(url: string): boolean {
const lower = url.toLowerCase();
@@ -37,22 +38,18 @@ export const DELETE: RequestHandler = async ({ url }) => {
return json({ error: 'Docker Hub does not support image deletion via API. Please use the Docker Hub web interface.' }, { status: 400 });
}
let baseUrl = registry.url;
if (!baseUrl.endsWith('/')) {
baseUrl += '/';
}
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull,push,delete`);
const headers: HeadersInit = {
'Accept': 'application/vnd.docker.distribution.manifest.v2+json'
};
if (registry.username && registry.password) {
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
if (authHeader) {
headers['Authorization'] = authHeader;
}
// Step 1: Get the manifest digest
const manifestUrl = `${baseUrl}v2/${imageName}/manifests/${tag}`;
const manifestUrl = `${baseUrl}/v2/${imageName}/manifests/${tag}`;
const headResponse = await fetch(manifestUrl, {
method: 'HEAD',
headers
@@ -74,7 +71,7 @@ export const DELETE: RequestHandler = async ({ url }) => {
}
// Step 2: Delete the manifest by digest
const deleteUrl = `${baseUrl}v2/${imageName}/manifests/${digest}`;
const deleteUrl = `${baseUrl}/v2/${imageName}/manifests/${digest}`;
const deleteResponse = await fetch(deleteUrl, {
method: 'DELETE',
headers
+116 -34
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getRegistry } from '$lib/server/db';
import { getRegistryAuth } from '$lib/server/docker';
interface SearchResult {
name: string;
@@ -44,46 +45,30 @@ async function searchDockerHub(term: string, limit: number): Promise<SearchResul
}
async function searchPrivateRegistry(registry: any, term: string, limit: number): Promise<SearchResult[]> {
// Private registries use the V2 catalog API
let baseUrl = registry.url;
if (!baseUrl.endsWith('/')) {
baseUrl += '/';
}
const results: string[] = [];
const catalogUrl = `${baseUrl}v2/_catalog?n=1000`;
const headers: HeadersInit = {
'Accept': 'application/json'
};
if (registry.username && registry.password) {
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
}
const response = await fetch(catalogUrl, {
method: 'GET',
headers
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication failed');
// Strategy 1: If term looks like an image name (contains /), try direct lookup first
// This is much faster than iterating through catalog for large registries like ghcr.io
if (term.includes('/')) {
const directResult = await tryDirectImageLookup(registry, term);
if (directResult) {
results.push(term);
}
throw new Error(`Registry returned error: ${response.status}`);
}
const data = await response.json();
const repositories = data.repositories || [];
// Filter repositories by search term (case-insensitive)
const termLower = term.toLowerCase();
const filtered = repositories
.filter((name: string) => name.toLowerCase().includes(termLower))
.slice(0, limit);
// Strategy 2: Fall back to catalog search for partial matches or if direct lookup failed
if (results.length < limit) {
const catalogResults = await searchCatalog(registry, term, limit - results.length);
// Add catalog results, avoiding duplicates
for (const name of catalogResults) {
if (!results.includes(name)) {
results.push(name);
}
}
}
// Return results in the same format as Docker Hub
return filtered.map((name: string) => ({
return results.map((name: string) => ({
name,
description: '',
star_count: 0,
@@ -92,6 +77,103 @@ async function searchPrivateRegistry(registry: any, term: string, limit: number)
}));
}
// Try to directly check if an image exists by querying its tags endpoint
async function tryDirectImageLookup(registry: any, imageName: string): Promise<boolean> {
try {
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
const headers: HeadersInit = {
'Accept': 'application/json'
};
if (authHeader) {
headers['Authorization'] = authHeader;
}
const response = await fetch(`${baseUrl}/v2/${imageName}/tags/list`, {
method: 'GET',
headers
});
// 200 = image exists, 404 = doesn't exist
return response.ok;
} catch {
return false;
}
}
// Search through catalog (slow for large registries, limited to first few pages)
async function searchCatalog(registry: any, term: string, limit: number): Promise<string[]> {
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
const headers: HeadersInit = {
'Accept': 'application/json'
};
if (authHeader) {
headers['Authorization'] = authHeader;
}
const termLower = term.toLowerCase();
const results: string[] = [];
const PAGE_SIZE = 200;
const MAX_PAGES = 3; // Limit pages to avoid long waits on huge registries
let lastRepo: string | null = null;
let pagesSearched = 0;
while (results.length < limit && pagesSearched < MAX_PAGES) {
let catalogUrl = `${baseUrl}/v2/_catalog?n=${PAGE_SIZE}`;
if (lastRepo) {
catalogUrl += `&last=${encodeURIComponent(lastRepo)}`;
}
const response = await fetch(catalogUrl, {
method: 'GET',
headers
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication failed');
}
throw new Error(`Registry returned error: ${response.status}`);
}
const data = await response.json();
const repositories: string[] = data.repositories || [];
if (repositories.length === 0) {
break;
}
// Filter and add matching repos
for (const name of repositories) {
if (name.toLowerCase().includes(termLower)) {
results.push(name);
if (results.length >= limit) {
break;
}
}
}
// Get last repo for next page
lastRepo = repositories[repositories.length - 1];
// Check if there are more pages
const linkHeader = response.headers.get('Link');
if (!linkHeader || !linkHeader.includes('rel="next"')) {
if (repositories.length < PAGE_SIZE) {
break;
}
}
pagesSearched++;
}
return results;
}
export const GET: RequestHandler = async ({ url }) => {
const term = url.searchParams.get('term');
const limit = parseInt(url.searchParams.get('limit') || '25', 10);
+5 -10
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getRegistry } from '$lib/server/db';
import { getRegistryAuth } from '$lib/server/docker';
interface TagInfo {
name: string;
@@ -72,21 +73,15 @@ async function fetchDockerHubTags(imageName: string, page: number = 1, pageSize:
}
async function fetchRegistryTags(registry: any, imageName: string): Promise<TagInfo[]> {
// Standard V2 registry API
let baseUrl = registry.url;
if (!baseUrl.endsWith('/')) {
baseUrl += '/';
}
const tagsUrl = `${baseUrl}v2/${imageName}/tags/list`;
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
const tagsUrl = `${baseUrl}/v2/${imageName}/tags/list`;
const headers: HeadersInit = {
'Accept': 'application/json'
};
if (registry.username && registry.password) {
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
if (authHeader) {
headers['Authorization'] = authHeader;
}
const response = await fetch(tagsUrl, {