Add web-based update system with detached process management (#65)
* feat: Add version checking and update functionality - Add version display component with GitHub release comparison - Implement update.sh script execution via API - Add hover tooltip with update instructions - Create shadcn/ui style Badge component - Add version router with getCurrentVersion, getLatestRelease, and executeUpdate endpoints - Update homepage header to show version and update status - Add Update Now button with loading states and result feedback - Support automatic page refresh after successful update * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Workflow * Workflow * Workflow * Update update script * Update update script * Update update script * Update update script * Update update script * Update update.sh * Update update.sh * Update update.sh * Update update.sh
This commit is contained in:
committed by
GitHub
parent
0b1ce29b64
commit
24430ee77d
10
package-lock.json
generated
10
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -7389,6 +7390,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.545.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
|
||||||
|
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
233
src/app/_components/VersionDisplay.tsx
Normal file
233
src/app/_components/VersionDisplay.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// Loading overlay component
|
||||||
|
function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-2xl border border-gray-200 dark:border-gray-700 max-w-md mx-4">
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-blue-600 dark:text-blue-400" />
|
||||||
|
<div className="absolute inset-0 rounded-full border-2 border-blue-200 dark:border-blue-800 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
{isNetworkError ? 'Server Restarting' : 'Updating Application'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{isNetworkError
|
||||||
|
? 'The server is restarting after the update...'
|
||||||
|
: 'Please stand by while we update your application...'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">
|
||||||
|
{isNetworkError
|
||||||
|
? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.'
|
||||||
|
: 'The server will restart automatically when complete.'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
|
||||||
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionDisplay() {
|
||||||
|
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
const [updateStartTime, setUpdateStartTime] = useState<number | null>(null);
|
||||||
|
const [isNetworkError, setIsNetworkError] = useState(false);
|
||||||
|
|
||||||
|
const executeUpdate = api.version.executeUpdate.useMutation({
|
||||||
|
onSuccess: (result: any) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = updateStartTime ? now - updateStartTime : 0;
|
||||||
|
|
||||||
|
|
||||||
|
setUpdateResult({ success: result.success, message: result.message });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// The script now runs independently, so we show a longer overlay
|
||||||
|
// and wait for the server to restart
|
||||||
|
setIsNetworkError(true);
|
||||||
|
setUpdateResult({ success: true, message: 'Update in progress... Server will restart automatically.' });
|
||||||
|
|
||||||
|
// Wait longer for the update to complete and server to restart
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUpdating(false);
|
||||||
|
setIsNetworkError(false);
|
||||||
|
// Try to reload after the update completes
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 10000); // 10 seconds to allow for update completion
|
||||||
|
}, 5000); // Show overlay for 5 seconds
|
||||||
|
} else {
|
||||||
|
// For errors, show for at least 1 second
|
||||||
|
const remainingTime = Math.max(0, 1000 - elapsed);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}, remainingTime);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = updateStartTime ? now - updateStartTime : 0;
|
||||||
|
|
||||||
|
// Check if this is a network error (expected during server restart)
|
||||||
|
const isNetworkError = error.message.includes('Failed to fetch') ||
|
||||||
|
error.message.includes('NetworkError') ||
|
||||||
|
error.message.includes('fetch') ||
|
||||||
|
error.message.includes('network');
|
||||||
|
|
||||||
|
if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success
|
||||||
|
setIsNetworkError(true);
|
||||||
|
setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' });
|
||||||
|
|
||||||
|
// Wait longer for server to come back up
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUpdating(false);
|
||||||
|
setIsNetworkError(false);
|
||||||
|
// Try to reload after a longer delay
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 5000);
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
// For real errors, show for at least 1 second
|
||||||
|
setUpdateResult({ success: false, message: error.message });
|
||||||
|
const remainingTime = Math.max(0, 1000 - elapsed);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}, remainingTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
setUpdateResult(null);
|
||||||
|
setIsNetworkError(false);
|
||||||
|
setUpdateStartTime(Date.now());
|
||||||
|
executeUpdate.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="secondary" className="animate-pulse">
|
||||||
|
Loading...
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !versionStatus?.success) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="destructive">
|
||||||
|
v{versionStatus?.currentVersion ?? 'Unknown'}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
(Unable to check for updates)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Loading overlay */}
|
||||||
|
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} />}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={isUpToDate ? "default" : "secondary"}>
|
||||||
|
v{currentVersion}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{updateAvailable && releaseInfo && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative group">
|
||||||
|
<Badge variant="destructive" className="animate-pulse cursor-help">
|
||||||
|
Update Available
|
||||||
|
</Badge>
|
||||||
|
<div className="absolute top-full left-1/2 transform -translate-x-1/2 mt-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-semibold mb-1">How to update:</div>
|
||||||
|
<div>Click the button to update</div>
|
||||||
|
<div>or update manually:</div>
|
||||||
|
<div>cd $PVESCRIPTLOCAL_DIR</div>
|
||||||
|
<div>git pull</div>
|
||||||
|
<div>npm install</div>
|
||||||
|
<div>npm run build</div>
|
||||||
|
<div>npm start</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdate}
|
||||||
|
disabled={isUpdating}
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className="text-xs h-6 px-2"
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
|
||||||
|
Updating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="h-3 w-3 mr-1" />
|
||||||
|
Update Now
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={releaseInfo.htmlUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="View latest release"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{updateResult && (
|
||||||
|
<div className={`text-xs px-2 py-1 rounded ${
|
||||||
|
updateResult.success
|
||||||
|
? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
|
||||||
|
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'
|
||||||
|
}`}>
|
||||||
|
{updateResult.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isUpToDate && (
|
||||||
|
<span className="text-xs text-green-600 dark:text-green-400">
|
||||||
|
✓ Up to date
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
src/app/_components/ui/badge.tsx
Normal file
28
src/app/_components/ui/badge.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
variant?: "default" | "secondary" | "destructive" | "outline"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||||
|
const variantClasses = {
|
||||||
|
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
variantClasses[variant],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge }
|
||||||
@@ -8,6 +8,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
|
|||||||
import { ResyncButton } from './_components/ResyncButton';
|
import { ResyncButton } from './_components/ResyncButton';
|
||||||
import { Terminal } from './_components/Terminal';
|
import { Terminal } from './_components/Terminal';
|
||||||
import { SettingsButton } from './_components/SettingsButton';
|
import { SettingsButton } from './_components/SettingsButton';
|
||||||
|
import { VersionDisplay } from './_components/VersionDisplay';
|
||||||
import { Button } from './_components/ui/button';
|
import { Button } from './_components/ui/button';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -30,9 +31,12 @@ export default function Home() {
|
|||||||
<h1 className="text-4xl font-bold text-foreground mb-2">
|
<h1 className="text-4xl font-bold text-foreground mb-2">
|
||||||
🚀 PVE Scripts Management
|
🚀 PVE Scripts Management
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground mb-4">
|
||||||
Manage and execute Proxmox helper scripts locally with live output streaming
|
Manage and execute Proxmox helper scripts locally with live output streaming
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<VersionDisplay />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { scriptsRouter } from "~/server/api/routers/scripts";
|
import { scriptsRouter } from "~/server/api/routers/scripts";
|
||||||
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
|
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
|
||||||
import { serversRouter } from "~/server/api/routers/servers";
|
import { serversRouter } from "~/server/api/routers/servers";
|
||||||
|
import { versionRouter } from "~/server/api/routers/version";
|
||||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
scripts: scriptsRouter,
|
scripts: scriptsRouter,
|
||||||
installedScripts: installedScriptsRouter,
|
installedScripts: installedScriptsRouter,
|
||||||
servers: serversRouter,
|
servers: serversRouter,
|
||||||
|
version: versionRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
148
src/server/api/routers/version.ts
Normal file
148
src/server/api/routers/version.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
interface GitHubRelease {
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
html_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versionRouter = createTRPCRouter({
|
||||||
|
// Get current local version
|
||||||
|
getCurrentVersion: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
try {
|
||||||
|
const versionPath = join(process.cwd(), 'VERSION');
|
||||||
|
const version = await readFile(versionPath, 'utf-8');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
version: version.trim()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading VERSION file:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to read VERSION file',
|
||||||
|
version: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getLatestRelease: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const release: GitHubRelease = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
release: {
|
||||||
|
tagName: release.tag_name,
|
||||||
|
name: release.name,
|
||||||
|
publishedAt: release.published_at,
|
||||||
|
htmlUrl: release.html_url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching latest release:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch latest release',
|
||||||
|
release: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
getVersionStatus: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const versionPath = join(process.cwd(), 'VERSION');
|
||||||
|
const currentVersion = (await readFile(versionPath, 'utf-8')).trim();
|
||||||
|
|
||||||
|
|
||||||
|
const response = await fetch('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const release: GitHubRelease = await response.json();
|
||||||
|
const latestVersion = release.tag_name.replace('v', '');
|
||||||
|
|
||||||
|
|
||||||
|
const isUpToDate = currentVersion === latestVersion;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion,
|
||||||
|
isUpToDate,
|
||||||
|
updateAvailable: !isUpToDate,
|
||||||
|
releaseInfo: {
|
||||||
|
tagName: release.tag_name,
|
||||||
|
name: release.name,
|
||||||
|
publishedAt: release.published_at,
|
||||||
|
htmlUrl: release.html_url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking version status:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to check version status',
|
||||||
|
currentVersion: null,
|
||||||
|
latestVersion: null,
|
||||||
|
isUpToDate: false,
|
||||||
|
updateAvailable: false,
|
||||||
|
releaseInfo: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Execute update script
|
||||||
|
executeUpdate: publicProcedure
|
||||||
|
.mutation(async () => {
|
||||||
|
try {
|
||||||
|
const updateScriptPath = join(process.cwd(), 'update.sh');
|
||||||
|
|
||||||
|
// Spawn the update script as a detached process using nohup
|
||||||
|
// This allows it to run independently and kill the parent Node.js process
|
||||||
|
const child = spawn('nohup', ['bash', updateScriptPath], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdio: ['ignore', 'ignore', 'ignore'],
|
||||||
|
shell: false,
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unref the child process so it doesn't keep the parent alive
|
||||||
|
child.unref();
|
||||||
|
|
||||||
|
// Immediately return success since we can't wait for completion
|
||||||
|
// The script will handle its own logging and restart
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Update started in background. The server will restart automatically when complete.',
|
||||||
|
output: '',
|
||||||
|
error: ''
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing update script:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to execute update script: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
895
update.sh
Executable file
895
update.sh
Executable file
@@ -0,0 +1,895 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Enhanced update script for ProxmoxVE-Local
|
||||||
|
# Fetches latest release from GitHub and backs up data directory
|
||||||
|
|
||||||
|
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
||||||
|
|
||||||
|
# Add error trap for debugging
|
||||||
|
trap 'echo "Error occurred at line $LINENO, command: $BASH_COMMAND"' ERR
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
REPO_OWNER="community-scripts"
|
||||||
|
REPO_NAME="ProxmoxVE-Local"
|
||||||
|
GITHUB_API="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}"
|
||||||
|
BACKUP_DIR="/tmp/pve-scripts-backup-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
DATA_DIR="./data"
|
||||||
|
LOG_FILE="/tmp/update.log"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Initialize log file
|
||||||
|
init_log() {
|
||||||
|
# Clear/create log file
|
||||||
|
> "$LOG_FILE"
|
||||||
|
log "Starting ProxmoxVE-Local update process..."
|
||||||
|
log "Log file: $LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging function
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if required tools are available
|
||||||
|
check_dependencies() {
|
||||||
|
log "Checking dependencies..."
|
||||||
|
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
missing_deps+=("curl")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
missing_deps+=("jq")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
missing_deps+=("npm")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
missing_deps+=("node")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -ne 0 ]; then
|
||||||
|
log_error "Missing dependencies: ${missing_deps[*]}"
|
||||||
|
log_error "Please install the missing dependencies and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "All dependencies are available"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get latest release info from GitHub API
|
||||||
|
get_latest_release() {
|
||||||
|
log "Fetching latest release information from GitHub..."
|
||||||
|
|
||||||
|
local release_info
|
||||||
|
if ! release_info=$(curl -s --connect-timeout 15 --max-time 60 --retry 2 --retry-delay 3 "$GITHUB_API/releases/latest"); then
|
||||||
|
log_error "Failed to fetch release information from GitHub API (timeout or network error)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if response is valid JSON
|
||||||
|
if ! echo "$release_info" | jq empty 2>/dev/null; then
|
||||||
|
log_error "Invalid JSON response from GitHub API"
|
||||||
|
log "Response: $release_info"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tag_name
|
||||||
|
local download_url
|
||||||
|
local published_at
|
||||||
|
|
||||||
|
tag_name=$(echo "$release_info" | jq -r '.tag_name')
|
||||||
|
download_url=$(echo "$release_info" | jq -r '.tarball_url')
|
||||||
|
published_at=$(echo "$release_info" | jq -r '.published_at')
|
||||||
|
|
||||||
|
if [ "$tag_name" = "null" ] || [ "$download_url" = "null" ] || [ -z "$tag_name" ] || [ -z "$download_url" ]; then
|
||||||
|
log_error "Failed to parse release information from API response"
|
||||||
|
log "Tag name: $tag_name"
|
||||||
|
log "Download URL: $download_url"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Latest release: $tag_name (published: $published_at)"
|
||||||
|
echo "$tag_name|$download_url"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backup data directory and .env file
|
||||||
|
backup_data() {
|
||||||
|
log "Creating backup directory at $BACKUP_DIR..."
|
||||||
|
|
||||||
|
if ! mkdir -p "$BACKUP_DIR"; then
|
||||||
|
log_error "Failed to create backup directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup data directory
|
||||||
|
if [ -d "$DATA_DIR" ]; then
|
||||||
|
log "Backing up data directory..."
|
||||||
|
|
||||||
|
if ! cp -r "$DATA_DIR" "$BACKUP_DIR/data"; then
|
||||||
|
log_error "Failed to backup data directory"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
log_success "Data directory backed up successfully"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "Data directory not found, skipping backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup .env file
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
log "Backing up .env file..."
|
||||||
|
if ! cp ".env" "$BACKUP_DIR/.env"; then
|
||||||
|
log_error "Failed to backup .env file"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
log_success ".env file backed up successfully"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning ".env file not found, skipping backup"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download and extract latest release
|
||||||
|
download_release() {
|
||||||
|
local release_info="$1"
|
||||||
|
local tag_name="${release_info%|*}"
|
||||||
|
local download_url="${release_info#*|}"
|
||||||
|
|
||||||
|
log "Downloading release $tag_name..."
|
||||||
|
|
||||||
|
local temp_dir="/tmp/pve-update-$$"
|
||||||
|
local archive_file="$temp_dir/release.tar.gz"
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
if ! mkdir -p "$temp_dir"; then
|
||||||
|
log_error "Failed to create temporary directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download release with timeout and progress
|
||||||
|
log "Downloading from: $download_url"
|
||||||
|
log "Target file: $archive_file"
|
||||||
|
log "Starting curl download..."
|
||||||
|
|
||||||
|
# Test if curl is working
|
||||||
|
log "Testing curl availability..."
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
log_error "curl command not found"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test basic connectivity
|
||||||
|
log "Testing basic connectivity..."
|
||||||
|
if ! curl -s --connect-timeout 10 --max-time 30 "https://api.github.com" >/dev/null 2>&1; then
|
||||||
|
log_error "Cannot reach GitHub API"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "Connectivity test passed"
|
||||||
|
|
||||||
|
# Create a temporary file for curl output
|
||||||
|
local curl_log="/tmp/curl_log_$$.txt"
|
||||||
|
|
||||||
|
# Run curl with verbose output
|
||||||
|
if curl -L --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -v -o "$archive_file" "$download_url" > "$curl_log" 2>&1; then
|
||||||
|
log_success "Curl command completed successfully"
|
||||||
|
# Show some of the curl output for debugging
|
||||||
|
log "Curl output (first 10 lines):"
|
||||||
|
head -10 "$curl_log" | while read -r line; do
|
||||||
|
log "CURL: $line"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
local curl_exit_code=$?
|
||||||
|
log_error "Curl command failed with exit code: $curl_exit_code"
|
||||||
|
log_error "Curl output:"
|
||||||
|
cat "$curl_log" | while read -r line; do
|
||||||
|
log_error "CURL: $line"
|
||||||
|
done
|
||||||
|
rm -f "$curl_log"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up curl log
|
||||||
|
rm -f "$curl_log"
|
||||||
|
|
||||||
|
# Verify download
|
||||||
|
if [ ! -f "$archive_file" ] || [ ! -s "$archive_file" ]; then
|
||||||
|
log_error "Downloaded file is empty or missing"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file_size
|
||||||
|
file_size=$(stat -c%s "$archive_file" 2>/dev/null || echo "0")
|
||||||
|
log_success "Downloaded release ($file_size bytes)"
|
||||||
|
|
||||||
|
# Extract release
|
||||||
|
log "Extracting release..."
|
||||||
|
if ! tar -xzf "$archive_file" -C "$temp_dir"; then
|
||||||
|
log_error "Failed to extract release"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debug: List contents after extraction
|
||||||
|
log "Contents after extraction:"
|
||||||
|
ls -la "$temp_dir" >&2 || true
|
||||||
|
|
||||||
|
# Find the extracted directory (GitHub tarballs have a root directory)
|
||||||
|
log "Looking for extracted directory with pattern: ${REPO_NAME}-*"
|
||||||
|
local extracted_dir
|
||||||
|
extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "${REPO_NAME}-*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
# If not found with repo name, try alternative patterns
|
||||||
|
if [ -z "$extracted_dir" ]; then
|
||||||
|
log "Trying pattern: community-scripts-ProxmoxVE-Local-*"
|
||||||
|
extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$extracted_dir" ]; then
|
||||||
|
log "Trying pattern: ProxmoxVE-Local-*"
|
||||||
|
extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d -name "ProxmoxVE-Local-*" 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$extracted_dir" ]; then
|
||||||
|
log "Trying any directory in temp folder"
|
||||||
|
extracted_dir=$(timeout 10 find "$temp_dir" -maxdepth 1 -type d ! -name "$temp_dir" 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If still not found, error out
|
||||||
|
if [ -z "$extracted_dir" ]; then
|
||||||
|
log_error "Could not find extracted directory"
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Found extracted directory: $extracted_dir"
|
||||||
|
log_success "Release downloaded and extracted successfully"
|
||||||
|
echo "$extracted_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear the original directory before updating
|
||||||
|
clear_original_directory() {
|
||||||
|
log "Clearing original directory..."
|
||||||
|
|
||||||
|
# List of files/directories to preserve (already backed up)
|
||||||
|
local preserve_patterns=(
|
||||||
|
"data"
|
||||||
|
".env"
|
||||||
|
"*.log"
|
||||||
|
"update.log"
|
||||||
|
"*.backup"
|
||||||
|
"*.bak"
|
||||||
|
"node_modules"
|
||||||
|
".git"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove all files except preserved ones
|
||||||
|
while IFS= read -r file; do
|
||||||
|
local should_preserve=false
|
||||||
|
local filename=$(basename "$file")
|
||||||
|
|
||||||
|
for pattern in "${preserve_patterns[@]}"; do
|
||||||
|
if [[ "$filename" == $pattern ]]; then
|
||||||
|
should_preserve=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_preserve" = false ]; then
|
||||||
|
rm -f "$file"
|
||||||
|
fi
|
||||||
|
done < <(find . -maxdepth 1 -type f ! -name ".*")
|
||||||
|
|
||||||
|
# Remove all directories except preserved ones
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
local should_preserve=false
|
||||||
|
local dirname=$(basename "$dir")
|
||||||
|
|
||||||
|
for pattern in "${preserve_patterns[@]}"; do
|
||||||
|
if [[ "$dirname" == $pattern ]]; then
|
||||||
|
should_preserve=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_preserve" = false ]; then
|
||||||
|
rm -rf "$dir"
|
||||||
|
fi
|
||||||
|
done < <(find . -maxdepth 1 -type d ! -name "." ! -name "..")
|
||||||
|
|
||||||
|
log_success "Original directory cleared"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restore backup files before building
|
||||||
|
restore_backup_files() {
|
||||||
|
log "Restoring .env and data directory from backup..."
|
||||||
|
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
# Restore .env file
|
||||||
|
if [ -f "$BACKUP_DIR/.env" ]; then
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
rm -f ".env"
|
||||||
|
fi
|
||||||
|
if mv "$BACKUP_DIR/.env" ".env"; then
|
||||||
|
log_success ".env file restored from backup"
|
||||||
|
else
|
||||||
|
log_error "Failed to restore .env file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "No .env file backup found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore data directory
|
||||||
|
if [ -d "$BACKUP_DIR/data" ]; then
|
||||||
|
if [ -d "data" ]; then
|
||||||
|
rm -rf "data"
|
||||||
|
fi
|
||||||
|
if mv "$BACKUP_DIR/data" "data"; then
|
||||||
|
log_success "Data directory restored from backup"
|
||||||
|
else
|
||||||
|
log_error "Failed to restore data directory"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "No data directory backup found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "No backup directory found for restoration"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if systemd service exists
|
||||||
|
check_service() {
|
||||||
|
if systemctl list-unit-files | grep -q "^pvescriptslocal.service"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kill application processes directly
|
||||||
|
kill_processes() {
|
||||||
|
# Try to find and stop the Node.js process
|
||||||
|
local pids
|
||||||
|
pids=$(pgrep -f "node server.js" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Also check for npm start processes
|
||||||
|
local npm_pids
|
||||||
|
npm_pids=$(pgrep -f "npm start" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Combine all PIDs
|
||||||
|
if [ -n "$npm_pids" ]; then
|
||||||
|
pids="$pids $npm_pids"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
log "Stopping application processes: $pids"
|
||||||
|
|
||||||
|
# Send TERM signal to each PID individually
|
||||||
|
for pid in $pids; do
|
||||||
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log "Sending TERM signal to PID: $pid"
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for graceful shutdown with timeout
|
||||||
|
log "Waiting for graceful shutdown..."
|
||||||
|
local wait_count=0
|
||||||
|
local max_wait=10 # Maximum 10 seconds
|
||||||
|
|
||||||
|
while [ $wait_count -lt $max_wait ]; do
|
||||||
|
local still_running
|
||||||
|
still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -z "$still_running" ]; then
|
||||||
|
log_success "Processes stopped gracefully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
wait_count=$((wait_count + 1))
|
||||||
|
log "Waiting... ($wait_count/$max_wait)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Force kill any remaining processes
|
||||||
|
local remaining_pids
|
||||||
|
remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$remaining_pids" ]; then
|
||||||
|
log_warning "Force killing remaining processes: $remaining_pids"
|
||||||
|
pkill -9 -f "node server.js" 2>/dev/null || true
|
||||||
|
pkill -9 -f "npm start" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
local final_check
|
||||||
|
final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$final_check" ]; then
|
||||||
|
log_warning "Some processes may still be running: $final_check"
|
||||||
|
else
|
||||||
|
log_success "All application processes stopped"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No running application processes found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kill application processes directly
|
||||||
|
kill_processes() {
|
||||||
|
# Try to find and stop the Node.js process
|
||||||
|
local pids
|
||||||
|
pids=$(pgrep -f "node server.js" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Also check for npm start processes
|
||||||
|
local npm_pids
|
||||||
|
npm_pids=$(pgrep -f "npm start" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Combine all PIDs
|
||||||
|
if [ -n "$npm_pids" ]; then
|
||||||
|
pids="$pids $npm_pids"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
log "Stopping application processes: $pids"
|
||||||
|
|
||||||
|
# Send TERM signal to each PID individually
|
||||||
|
for pid in $pids; do
|
||||||
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log "Sending TERM signal to PID: $pid"
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait for graceful shutdown with timeout
|
||||||
|
log "Waiting for graceful shutdown..."
|
||||||
|
local wait_count=0
|
||||||
|
local max_wait=10 # Maximum 10 seconds
|
||||||
|
|
||||||
|
while [ $wait_count -lt $max_wait ]; do
|
||||||
|
local still_running
|
||||||
|
still_running=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -z "$still_running" ]; then
|
||||||
|
log_success "Processes stopped gracefully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
wait_count=$((wait_count + 1))
|
||||||
|
log "Waiting... ($wait_count/$max_wait)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Force kill any remaining processes
|
||||||
|
local remaining_pids
|
||||||
|
remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$remaining_pids" ]; then
|
||||||
|
log_warning "Force killing remaining processes: $remaining_pids"
|
||||||
|
pkill -9 -f "node server.js" 2>/dev/null || true
|
||||||
|
pkill -9 -f "npm start" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
local final_check
|
||||||
|
final_check=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$final_check" ]; then
|
||||||
|
log_warning "Some processes may still be running: $final_check"
|
||||||
|
else
|
||||||
|
log_success "All application processes stopped"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No running application processes found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop the application before updating
|
||||||
|
stop_application() {
|
||||||
|
log "Stopping application..."
|
||||||
|
|
||||||
|
# Change to the application directory if we're not already there
|
||||||
|
local app_dir
|
||||||
|
if [ -f "package.json" ] && [ -f "server.js" ]; then
|
||||||
|
app_dir="$(pwd)"
|
||||||
|
else
|
||||||
|
# Try to find the application directory
|
||||||
|
app_dir=$(find /root -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
|
||||||
|
cd "$app_dir" || {
|
||||||
|
log_error "Failed to change to application directory: $app_dir"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log_error "Could not find application directory"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Working from application directory: $(pwd)"
|
||||||
|
|
||||||
|
# Check if systemd service exists and is active
|
||||||
|
if check_service; then
|
||||||
|
if systemctl is-active --quiet pvescriptslocal.service; then
|
||||||
|
log "Stopping pvescriptslocal service..."
|
||||||
|
if systemctl stop pvescriptslocal.service; then
|
||||||
|
log_success "Service stopped successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to stop service, falling back to process kill"
|
||||||
|
kill_processes
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Service exists but is not active, checking for running processes..."
|
||||||
|
kill_processes
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No systemd service found, stopping processes directly..."
|
||||||
|
kill_processes
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update application files
|
||||||
|
update_files() {
|
||||||
|
local source_dir="$1"
|
||||||
|
|
||||||
|
log "Updating application files..."
|
||||||
|
|
||||||
|
# List of files/directories to exclude from update
|
||||||
|
local exclude_patterns=(
|
||||||
|
"data"
|
||||||
|
"node_modules"
|
||||||
|
".git"
|
||||||
|
".env"
|
||||||
|
"*.log"
|
||||||
|
"update.log"
|
||||||
|
"*.backup"
|
||||||
|
"*.bak"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find the actual source directory (strip the top-level directory)
|
||||||
|
local actual_source_dir
|
||||||
|
actual_source_dir=$(find "$source_dir" -maxdepth 1 -type d -name "community-scripts-ProxmoxVE-Local-*" | head -1)
|
||||||
|
|
||||||
|
if [ -z "$actual_source_dir" ]; then
|
||||||
|
log_error "Could not find the actual source directory in $source_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use process substitution instead of pipe to avoid subshell issues
|
||||||
|
local files_copied=0
|
||||||
|
local files_excluded=0
|
||||||
|
|
||||||
|
log "Starting file copy process from: $actual_source_dir"
|
||||||
|
|
||||||
|
# Create a temporary file list to avoid process substitution issues
|
||||||
|
local file_list="/tmp/file_list_$$.txt"
|
||||||
|
find "$actual_source_dir" -type f > "$file_list"
|
||||||
|
|
||||||
|
local total_files
|
||||||
|
total_files=$(wc -l < "$file_list")
|
||||||
|
log "Found $total_files files to process"
|
||||||
|
|
||||||
|
# Show first few files for debugging
|
||||||
|
log "First few files to process:"
|
||||||
|
head -5 "$file_list" | while read -r f; do
|
||||||
|
log " - $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r file; do
|
||||||
|
local rel_path="${file#$actual_source_dir/}"
|
||||||
|
local should_exclude=false
|
||||||
|
|
||||||
|
for pattern in "${exclude_patterns[@]}"; do
|
||||||
|
if [[ "$rel_path" == $pattern ]]; then
|
||||||
|
should_exclude=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_exclude" = false ]; then
|
||||||
|
local target_dir
|
||||||
|
target_dir=$(dirname "$rel_path")
|
||||||
|
if [ "$target_dir" != "." ]; then
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
fi
|
||||||
|
log "Copying: $file -> $rel_path"
|
||||||
|
if ! cp "$file" "$rel_path"; then
|
||||||
|
log_error "Failed to copy $rel_path"
|
||||||
|
rm -f "$file_list"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
files_copied=$((files_copied + 1))
|
||||||
|
if [ $((files_copied % 10)) -eq 0 ]; then
|
||||||
|
log "Copied $files_copied files so far..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
files_excluded=$((files_excluded + 1))
|
||||||
|
log "Excluded: $rel_path"
|
||||||
|
fi
|
||||||
|
done < "$file_list"
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
rm -f "$file_list"
|
||||||
|
|
||||||
|
log "Files processed: $files_copied copied, $files_excluded excluded"
|
||||||
|
|
||||||
|
log_success "Application files updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install dependencies and build
|
||||||
|
install_and_build() {
|
||||||
|
log "Installing dependencies..."
|
||||||
|
|
||||||
|
if ! npm install; then
|
||||||
|
log_error "Failed to install dependencies"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure no processes are running before build
|
||||||
|
log "Ensuring no conflicting processes are running..."
|
||||||
|
local pids
|
||||||
|
pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
log_warning "Found running processes, stopping them: $pids"
|
||||||
|
pkill -9 -f "node server.js" 2>/dev/null || true
|
||||||
|
pkill -9 -f "npm start" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Building application..."
|
||||||
|
# Set NODE_ENV to production for build
|
||||||
|
export NODE_ENV=production
|
||||||
|
|
||||||
|
if ! npm run build; then
|
||||||
|
log_error "Failed to build application"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Dependencies installed and application built successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the application after updating
|
||||||
|
start_application() {
|
||||||
|
log "Starting application..."
|
||||||
|
|
||||||
|
# Check if systemd service exists
|
||||||
|
if check_service; then
|
||||||
|
log "Starting pvescriptslocal service..."
|
||||||
|
if systemctl start pvescriptslocal.service; then
|
||||||
|
log_success "Service started successfully"
|
||||||
|
# Wait a moment and check if it's running
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active --quiet pvescriptslocal.service; then
|
||||||
|
log_success "Service is running"
|
||||||
|
else
|
||||||
|
log_warning "Service started but may not be running properly"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Failed to start service, falling back to npm start"
|
||||||
|
start_with_npm
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "No systemd service found, starting with npm..."
|
||||||
|
start_with_npm
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start application with npm
|
||||||
|
start_with_npm() {
|
||||||
|
log "Starting application with npm start..."
|
||||||
|
|
||||||
|
# Start in background
|
||||||
|
nohup npm start > server.log 2>&1 &
|
||||||
|
local npm_pid=$!
|
||||||
|
|
||||||
|
# Wait a moment and check if it started
|
||||||
|
sleep 3
|
||||||
|
if kill -0 $npm_pid 2>/dev/null; then
|
||||||
|
log_success "Application started with PID: $npm_pid"
|
||||||
|
else
|
||||||
|
log_error "Failed to start application with npm"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rollback function
|
||||||
|
rollback() {
|
||||||
|
log_warning "Rolling back to previous version..."
|
||||||
|
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
log "Restoring from backup directory: $BACKUP_DIR"
|
||||||
|
|
||||||
|
# Restore data directory
|
||||||
|
if [ -d "$BACKUP_DIR/data" ]; then
|
||||||
|
log "Restoring data directory..."
|
||||||
|
if [ -d "$DATA_DIR" ]; then
|
||||||
|
rm -rf "$DATA_DIR"
|
||||||
|
fi
|
||||||
|
if mv "$BACKUP_DIR/data" "$DATA_DIR"; then
|
||||||
|
log_success "Data directory restored from backup"
|
||||||
|
else
|
||||||
|
log_error "Failed to restore data directory"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "No data directory backup found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore .env file
|
||||||
|
if [ -f "$BACKUP_DIR/.env" ]; then
|
||||||
|
log "Restoring .env file..."
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
rm -f ".env"
|
||||||
|
fi
|
||||||
|
if mv "$BACKUP_DIR/.env" ".env"; then
|
||||||
|
log_success ".env file restored from backup"
|
||||||
|
else
|
||||||
|
log_error "Failed to restore .env file"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "No .env file backup found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up backup directory
|
||||||
|
log "Cleaning up backup directory..."
|
||||||
|
rm -rf "$BACKUP_DIR"
|
||||||
|
else
|
||||||
|
log_error "No backup directory found for rollback"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_error "Update failed. Please check the logs and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main update process
|
||||||
|
main() {
|
||||||
|
init_log
|
||||||
|
|
||||||
|
# Check if we're running from the application directory and not already relocated
|
||||||
|
if [ -z "${PVE_UPDATE_RELOCATED:-}" ] && [ -f "package.json" ] && [ -f "server.js" ]; then
|
||||||
|
log "Detected running from application directory"
|
||||||
|
log "Copying update script to temporary location for safe execution..."
|
||||||
|
|
||||||
|
local temp_script="/tmp/pve-scripts-update-$$.sh"
|
||||||
|
if ! cp "$0" "$temp_script"; then
|
||||||
|
log_error "Failed to copy update script to temporary location"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$temp_script"
|
||||||
|
log "Executing update from temporary location: $temp_script"
|
||||||
|
|
||||||
|
# Set flag to prevent infinite loop and execute from temporary location
|
||||||
|
export PVE_UPDATE_RELOCATED=1
|
||||||
|
exec "$temp_script" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure we're in the application directory
|
||||||
|
local app_dir
|
||||||
|
|
||||||
|
# First check if we're already in the right directory
|
||||||
|
if [ -f "package.json" ] && [ -f "server.js" ]; then
|
||||||
|
app_dir="$(pwd)"
|
||||||
|
log "Already in application directory: $app_dir"
|
||||||
|
else
|
||||||
|
# Try multiple common locations
|
||||||
|
for search_path in /opt /root /home /usr/local; do
|
||||||
|
if [ -d "$search_path" ]; then
|
||||||
|
app_dir=$(find "$search_path" -name "package.json" -path "*/ProxmoxVE-Local*" -exec dirname {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$app_dir" ] && [ -d "$app_dir" ]; then
|
||||||
|
cd "$app_dir" || {
|
||||||
|
log_error "Failed to change to application directory: $app_dir"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
log "Changed to application directory: $(pwd)"
|
||||||
|
else
|
||||||
|
log_error "Could not find application directory"
|
||||||
|
log "Searched in: /opt, /root, /home, /usr/local"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
check_dependencies
|
||||||
|
|
||||||
|
# Get latest release info
|
||||||
|
local release_info
|
||||||
|
release_info=$(get_latest_release)
|
||||||
|
|
||||||
|
# Backup data directory
|
||||||
|
backup_data
|
||||||
|
|
||||||
|
# Stop the application before updating (now running from /tmp/)
|
||||||
|
stop_application
|
||||||
|
|
||||||
|
# Double-check that no processes are running
|
||||||
|
local remaining_pids
|
||||||
|
remaining_pids=$(pgrep -f "node server.js\|npm start" 2>/dev/null || true)
|
||||||
|
if [ -n "$remaining_pids" ]; then
|
||||||
|
log_warning "Force killing remaining processes"
|
||||||
|
pkill -9 -f "node server.js" 2>/dev/null || true
|
||||||
|
pkill -9 -f "npm start" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download and extract release
|
||||||
|
local source_dir
|
||||||
|
source_dir=$(download_release "$release_info")
|
||||||
|
log "Download completed, source_dir: $source_dir"
|
||||||
|
|
||||||
|
# Clear the original directory before updating
|
||||||
|
log "Clearing original directory..."
|
||||||
|
clear_original_directory
|
||||||
|
log "Original directory cleared successfully"
|
||||||
|
|
||||||
|
# Update files
|
||||||
|
log "Starting file update process..."
|
||||||
|
if ! update_files "$source_dir"; then
|
||||||
|
log_error "File update failed, rolling back..."
|
||||||
|
rollback
|
||||||
|
fi
|
||||||
|
log "File update completed successfully"
|
||||||
|
|
||||||
|
# Restore .env and data directory before building
|
||||||
|
log "Restoring backup files..."
|
||||||
|
restore_backup_files
|
||||||
|
log "Backup files restored successfully"
|
||||||
|
|
||||||
|
# Install dependencies and build
|
||||||
|
log "Starting install and build process..."
|
||||||
|
if ! install_and_build; then
|
||||||
|
log_error "Install and build failed, rolling back..."
|
||||||
|
rollback
|
||||||
|
fi
|
||||||
|
log "Install and build completed successfully"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
log "Cleaning up temporary files..."
|
||||||
|
rm -rf "$source_dir"
|
||||||
|
rm -rf "/tmp/pve-update-$$"
|
||||||
|
|
||||||
|
# Clean up temporary script if it exists
|
||||||
|
if [ -f "/tmp/pve-scripts-update-$$.sh" ]; then
|
||||||
|
rm -f "/tmp/pve-scripts-update-$$.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
start_application
|
||||||
|
|
||||||
|
log_success "Update completed successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function with error handling
|
||||||
|
if ! main "$@"; then
|
||||||
|
log_error "Update script failed with exit code $?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user