1.0.30
This commit is contained in:
49 files changed
+1414
-492
No files matched your search
@@ -0,0 +1,29 @@
|
||||
export interface ParsedCustomUrl {
|
||||
url: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a custom URL value that may be in markdown link format.
|
||||
* Supports:
|
||||
* - Plain URL: "https://example.com" → { url: "https://example.com" }
|
||||
* - Markdown link: "[My App](https://example.com)" → { url: "https://example.com", name: "My App" }
|
||||
* Returns null if value is empty/missing.
|
||||
*/
|
||||
export function parseCustomUrl(value: string | undefined | null): ParsedCustomUrl | null {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const match = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
||||
if (match) {
|
||||
return { name: match[1].trim(), url: ensureProtocol(match[2].trim()) };
|
||||
}
|
||||
|
||||
return { url: ensureProtocol(trimmed) };
|
||||
}
|
||||
|
||||
function ensureProtocol(url: string): string {
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
return `https://${url}`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function formatBytes(bytes: number, decimals = 1): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : decimals)} ${units[i]}`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function wrapHtmlLines(html: string): string {
|
||||
return html
|
||||
.split('\n')
|
||||
.map((line) => `<div class="log-line">${line || ' '}</div>`)
|
||||
.join('');
|
||||
}
|
||||
@@ -22,15 +22,35 @@ export function parseTelegramUrl(url: string): { botToken: string; chatId: strin
|
||||
|
||||
// --- Gotify ---
|
||||
|
||||
export function buildGotifyUrl(appriseUrl: string): string | null {
|
||||
const match = appriseUrl.match(/^gotifys?:\/\/([^/]+)\/(.+)/);
|
||||
export function buildGotifyUrl(appriseUrl: string): { url: string; priority?: number } | null {
|
||||
// Strip query params before parsing path
|
||||
const qIdx = appriseUrl.indexOf('?');
|
||||
const baseUrl = qIdx >= 0 ? appriseUrl.substring(0, qIdx) : appriseUrl;
|
||||
const queryStr = qIdx >= 0 ? appriseUrl.substring(qIdx + 1) : '';
|
||||
|
||||
const match = baseUrl.match(/^gotifys?:\/\/([^/]+)\/(.+)/);
|
||||
if (!match) return null;
|
||||
const [, hostname, pathPart] = match;
|
||||
const protocol = appriseUrl.startsWith('gotifys') ? 'https' : 'http';
|
||||
const lastSlash = pathPart.lastIndexOf('/');
|
||||
const subpath = lastSlash >= 0 ? pathPart.substring(0, lastSlash) : '';
|
||||
const token = lastSlash >= 0 ? pathPart.substring(lastSlash + 1) : pathPart;
|
||||
return `${protocol}://${hostname}${subpath ? '/' + subpath : ''}/message?token=${token}`;
|
||||
|
||||
// Parse priority from query params
|
||||
let priority: number | undefined;
|
||||
if (queryStr) {
|
||||
const params = new URLSearchParams(queryStr);
|
||||
const p = params.get('priority');
|
||||
if (p) {
|
||||
const num = parseInt(p);
|
||||
if (!isNaN(num) && num >= 0 && num <= 10) priority = num;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: `${protocol}://${hostname}${subpath ? '/' + subpath : ''}/message?token=${token}`,
|
||||
priority
|
||||
};
|
||||
}
|
||||
|
||||
// --- Workflows (Microsoft Power Automate) ---
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Parse Docker port mapping syntax from UI inputs.
|
||||
*
|
||||
* Supported host port formats:
|
||||
* "8080" -> { hostIp: '', hostPort: '8080' }
|
||||
* "127.0.0.1:8080" -> { hostIp: '127.0.0.1', hostPort: '8080' }
|
||||
* "::1:8080" -> { hostIp: '::1', hostPort: '8080' }
|
||||
* "[::1]:8080" -> { hostIp: '::1', hostPort: '8080' }
|
||||
* "" -> { hostIp: '', hostPort: '' } (empty = random)
|
||||
*
|
||||
* Supported container port formats:
|
||||
* "8080" -> single port
|
||||
* "8000-8005" -> port range
|
||||
*/
|
||||
|
||||
export interface ParsedHostPort {
|
||||
hostIp: string;
|
||||
hostPort: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a host port string that may include an IP binding.
|
||||
* Returns the IP and port separately.
|
||||
*/
|
||||
export function parseHostPort(input: string): ParsedHostPort {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return { hostIp: '', hostPort: '' };
|
||||
|
||||
// Check for bracketed IPv6: [::1]:8080
|
||||
const bracketMatch = trimmed.match(/^\[([^\]]+)\]:(.+)$/);
|
||||
if (bracketMatch) {
|
||||
return { hostIp: bracketMatch[1], hostPort: bracketMatch[2] };
|
||||
}
|
||||
|
||||
// Count colons to distinguish IPv4:port from IPv6
|
||||
const colons = (trimmed.match(/:/g) || []).length;
|
||||
|
||||
if (colons === 0) {
|
||||
// Just a port number or range: "8080" or "8000-8005"
|
||||
return { hostIp: '', hostPort: trimmed };
|
||||
}
|
||||
|
||||
if (colons === 1) {
|
||||
// IPv4:port -> "127.0.0.1:8080"
|
||||
const lastColon = trimmed.lastIndexOf(':');
|
||||
return {
|
||||
hostIp: trimmed.substring(0, lastColon),
|
||||
hostPort: trimmed.substring(lastColon + 1)
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple colons — ambiguous between IPv6 address and IPv6:port.
|
||||
// Use bracket notation [::1]:8080 for IPv6 with port.
|
||||
// Bare multi-colon input is treated as IPv6 address with no port.
|
||||
return { hostIp: trimmed, hostPort: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a port number or range string.
|
||||
* Returns error message or empty string if valid.
|
||||
*/
|
||||
export function validatePort(port: string): string {
|
||||
if (!port) return ''; // Empty is valid (means random allocation)
|
||||
|
||||
// Port range: "8000-8005"
|
||||
const rangeMatch = port.match(/^(\d+)-(\d+)$/);
|
||||
if (rangeMatch) {
|
||||
const start = parseInt(rangeMatch[1]);
|
||||
const end = parseInt(rangeMatch[2]);
|
||||
if (start < 1 || start > 65535) return `Port ${start} out of range (1-65535)`;
|
||||
if (end < 1 || end > 65535) return `Port ${end} out of range (1-65535)`;
|
||||
if (start >= end) return 'Range start must be less than end';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Single port
|
||||
if (!/^\d+$/.test(port)) return 'Invalid port number';
|
||||
const num = parseInt(port);
|
||||
if (num < 1 || num > 65535) return 'Port out of range (1-65535)';
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an IP address (IPv4 or IPv6).
|
||||
* Returns error message or empty string if valid.
|
||||
*/
|
||||
export function validateIp(ip: string): string {
|
||||
if (!ip) return ''; // Empty is valid (bind to all interfaces)
|
||||
|
||||
// Basic IPv4 check
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
||||
const parts = ip.split('.').map(Number);
|
||||
if (parts.every(p => p >= 0 && p <= 255)) return '';
|
||||
return 'Invalid IPv4 address';
|
||||
}
|
||||
|
||||
// IPv6 (simplified check — accept common forms)
|
||||
if (/^[0-9a-fA-F:]+$/.test(ip)) return '';
|
||||
|
||||
return 'Invalid IP address';
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand port range mappings into individual Docker API port bindings.
|
||||
*
|
||||
* Input: hostPort="8000-8005", containerPort="9000-9005", protocol="tcp", hostIp=""
|
||||
* Output: { "9000/tcp": { HostPort: "8000" }, "9001/tcp": { HostPort: "8001" }, ... }
|
||||
*/
|
||||
export function expandPortBindings(
|
||||
hostPort: string,
|
||||
containerPort: string,
|
||||
protocol: string,
|
||||
hostIp: string
|
||||
): Record<string, { HostIp?: string; HostPort: string }> {
|
||||
const result: Record<string, { HostIp?: string; HostPort: string }> = {};
|
||||
|
||||
const hostRange = parseRange(hostPort);
|
||||
const containerRange = parseRange(containerPort);
|
||||
|
||||
if (hostRange && containerRange) {
|
||||
// Both are ranges — must be same length
|
||||
const len = Math.min(hostRange.length, containerRange.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const key = `${containerRange[i]}/${protocol}`;
|
||||
const binding: { HostIp?: string; HostPort: string } = { HostPort: String(hostRange[i]) };
|
||||
if (hostIp) binding.HostIp = hostIp;
|
||||
result[key] = binding;
|
||||
}
|
||||
} else if (containerRange) {
|
||||
// Container is range, host is single port or empty
|
||||
for (const cp of containerRange) {
|
||||
const key = `${cp}/${protocol}`;
|
||||
const binding: { HostIp?: string; HostPort: string } = { HostPort: hostPort || '0' };
|
||||
if (hostIp) binding.HostIp = hostIp;
|
||||
result[key] = binding;
|
||||
}
|
||||
} else {
|
||||
// Single port mapping
|
||||
const key = `${containerPort}/${protocol}`;
|
||||
const binding: { HostIp?: string; HostPort: string } = { HostPort: hostPort || '0' };
|
||||
if (hostIp) binding.HostIp = hostIp;
|
||||
result[key] = binding;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseRange(port: string): number[] | null {
|
||||
const match = port.match(/^(\d+)-(\d+)$/);
|
||||
if (!match) return null;
|
||||
const start = parseInt(match[1]);
|
||||
const end = parseInt(match[2]);
|
||||
const result: number[] = [];
|
||||
for (let i = start; i <= end; i++) result.push(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a parsed host port back to display string.
|
||||
*/
|
||||
export function formatHostPort(hostIp: string, hostPort: string): string {
|
||||
if (!hostIp) return hostPort;
|
||||
// IPv6 needs brackets
|
||||
if (hostIp.includes(':')) return `[${hostIp}]:${hostPort}`;
|
||||
return `${hostIp}:${hostPort}`;
|
||||
}
|
||||
Reference in new issue
Block a user