Support -f on service create and redeploy across all SDKs

Add input_files support to service redeploy for all 12 SDK
implementations that have service commands. Files passed via -f are
read, base64-encoded, and sent as input_files in the JSON payload.
Service create also gains -f support where it was missing.

Updated: bash, cpp, csharp, dotnet, go, java, javascript, perl,
php, ruby, rust, typescript
This commit is contained in:
russell@unturf.com 2026-03-02 06:13:43 -05:00
parent ac50544ac3
commit 369f3aa39d
13 changed files with 790 additions and 46 deletions

View file

@ -424,6 +424,7 @@ service_create() {
local name="$1"
local ports="${2:-}"
local bootstrap="${3:-}"
local input_files_json="${4:-}"
local body
body=$(jq -n --arg name "$name" '{name: $name}')
@ -434,6 +435,9 @@ service_create() {
if [ -n "$bootstrap" ]; then
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
fi
if [ -n "$input_files_json" ]; then
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
fi
api_request "POST" "/services" "$body"
}
@ -472,10 +476,14 @@ service_set_unfreeze_on_demand() {
service_redeploy() {
local service_id="$1"
local bootstrap="${2:-}"
local input_files_json="${3:-}"
local body="{}"
if [ -n "$bootstrap" ]; then
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
fi
if [ -n "$input_files_json" ]; then
body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}')
fi
api_request "POST" "/services/$service_id/redeploy" "$body"
}
@ -902,6 +910,9 @@ cmd_service() {
local target=""
local name=""
local ports=""
local bootstrap=""
local bootstrap_file=""
local -a files=()
while [ $# -gt 0 ]; do
case "$1" in
@ -913,13 +924,47 @@ cmd_service() {
--lock) action="lock"; target="$2"; shift ;;
--unlock) action="unlock"; target="$2"; shift ;;
--logs) action="logs"; target="$2"; shift ;;
--redeploy) action="redeploy"; target="$2"; shift ;;
--name) name="$2"; shift ;;
--ports) ports="$2"; shift ;;
--bootstrap) bootstrap="$2"; shift ;;
--bootstrap-file) bootstrap_file="$2"; shift ;;
-f|--file) files+=("$2"); shift ;;
*) ;;
esac
shift
done
# Build input_files JSON from -f args
local input_files_json=""
if [ ${#files[@]} -gt 0 ]; then
input_files_json="["
local first=1
for fpath in "${files[@]}"; do
if [ ! -f "$fpath" ]; then
echo -e "${RED}Error: File not found: $fpath${RESET}" >&2
exit 1
fi
local encoded
encoded=$(base64 -w0 "$fpath" 2>/dev/null || base64 "$fpath" 2>/dev/null)
local fname
fname=$(basename "$fpath")
[ "$first" -eq 0 ] && input_files_json="$input_files_json,"
input_files_json="$input_files_json{\"filename\":$(echo "$fname" | jq -Rs .),\"content\":$(echo "$encoded" | jq -Rs .)}"
first=0
done
input_files_json="$input_files_json]"
fi
# Resolve bootstrap from file if provided
if [ -n "$bootstrap_file" ]; then
if [ ! -f "$bootstrap_file" ]; then
echo -e "${RED}Error: Bootstrap file not found: $bootstrap_file${RESET}" >&2
exit 1
fi
bootstrap=$(cat "$bootstrap_file")
fi
case "$action" in
list)
local result
@ -954,14 +999,19 @@ cmd_service() {
result=$(service_logs "$target")
echo "$result" | jq -r '.logs // empty'
;;
redeploy)
local result
result=$(service_redeploy "$target" "$bootstrap" "$input_files_json")
echo -e "${GREEN}Service redeployed: $target${RESET}"
;;
*)
if [ -n "$name" ]; then
local result
result=$(service_create "$name" "$ports" "")
result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json")
echo -e "${GREEN}Service created${RESET}"
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
else
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--name NAME" >&2
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--redeploy ID|--name NAME" >&2
exit 1
fi
;;
@ -1138,8 +1188,12 @@ Service options:
--lock ID Lock service
--unlock ID Unlock service
--logs ID Get service logs
--redeploy ID Re-run bootstrap (supports -f, --bootstrap)
--name NAME Create service with name
--ports PORTS Service ports (comma-separated)
--bootstrap CMD Bootstrap command
--bootstrap-file FILE Bootstrap from file
-f, --file FILE Add input file (can repeat)
Snapshot options:
--list List all snapshots

View file

@ -705,9 +705,32 @@ string service_unlock(const string& service_id, const string& public_key, const
return exec_curl(cmd);
}
string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) {
string service_redeploy(const string& service_id, const string& bootstrap, const vector<string>& input_files, const string& public_key, const string& secret_key) {
string path = "/services/" + service_id + "/redeploy";
string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}";
ostringstream json;
json << "{";
bool has_field = false;
if (!bootstrap.empty()) {
json << "\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
has_field = true;
}
if (!input_files.empty()) {
if (has_field) json << ",";
json << "\"input_files\":[";
for (size_t i = 0; i < input_files.size(); i++) {
if (i > 0) json << ",";
ifstream file(input_files[i], ios::binary);
if (!file) continue;
ostringstream content;
content << file.rdbuf();
string b64 = base64_encode(content.str());
string filename = input_files[i].substr(input_files[i].find_last_of("/\\") + 1);
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
}
json << "]";
}
json << "}";
string body = json.str();
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
"-H 'Content-Type: application/json' "
@ -1202,7 +1225,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
cout << exec_curl(cmd) << endl;
}
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) {
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& redeploy, const string& network, int vcpu, const vector<string>& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) {
// Handle service env subcommand
if (!env_action.empty()) {
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
@ -1328,6 +1351,62 @@ void cmd_service(const string& name, const string& ports, const string& type, co
return;
}
if (!redeploy.empty()) {
// Bootstrap is optional for redeploy:
// - If provided via --bootstrap or --bootstrap-file, use it
// - If omitted, API will use the stored encrypted bootstrap
string bootstrap_to_use = bootstrap;
if (!bootstrap_file.empty()) {
struct stat st;
if (stat(bootstrap_file.c_str(), &st) == 0) {
bootstrap_to_use = read_file(bootstrap_file);
} else {
cerr << RED << "Error: Bootstrap file not found: " << bootstrap_file << RESET << endl;
exit(1);
}
}
cout << YELLOW << "Redeploying service " << redeploy << "..." << RESET << endl;
ostringstream json;
json << "{";
bool has_field = false;
if (!bootstrap_to_use.empty()) {
if (!bootstrap_file.empty()) {
json << "\"bootstrap_content\":\"" << escape_json(bootstrap_to_use) << "\"";
} else {
json << "\"bootstrap\":\"" << escape_json(bootstrap_to_use) << "\"";
}
has_field = true;
}
if (!files.empty()) {
if (has_field) json << ",";
json << "\"input_files\":[";
for (size_t i = 0; i < files.size(); i++) {
if (i > 0) json << ",";
ifstream file(files[i], ios::binary);
if (!file) {
cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl;
exit(1);
}
ostringstream content;
content << file.rdbuf();
string b64 = base64_encode(content.str());
string filename = files[i].substr(files[i].find_last_of("/\\") + 1);
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
}
json << "]";
}
json << "}";
string path = "/services/" + redeploy + "/redeploy";
string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key);
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
"-H 'Content-Type: application/json' "
+ auth_headers + " "
"-d '" + json.str() + "'";
string result = exec_curl(cmd);
cout << result << endl;
return;
}
if (!dump_bootstrap.empty()) {
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
@ -1778,7 +1857,7 @@ int main(int argc, char* argv[]) {
if (cmd_type == "service") {
string name, ports, type, bootstrap, bootstrap_file;
bool list = false;
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network;
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, redeploy;
int vcpu = 0;
vector<string> files;
vector<string> envs;
@ -1816,6 +1895,7 @@ int main(int argc, char* argv[]) {
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
else if (arg == "--execute" && i+1 < argc) execute = argv[++i];
else if (arg == "--command" && i+1 < argc) command = argv[++i];
else if (arg == "--redeploy" && i+1 < argc) redeploy = argv[++i];
else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i];
else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i];
else if (arg == "-n" && i+1 < argc) network = argv[++i];
@ -1832,7 +1912,7 @@ int main(int argc, char* argv[]) {
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
}
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key);
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, redeploy, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key);
return 0;
}

View file

@ -92,6 +92,10 @@ class Un
{
CmdKey(parsedArgs);
}
else if (parsedArgs.Command == "languages")
{
CmdLanguages(parsedArgs);
}
else if (parsedArgs.SourceFile != null)
{
CmdExecute(parsedArgs);
@ -438,6 +442,32 @@ class Un
return;
}
if (args.ServiceRedeploy != null)
{
var payload = new Dictionary<string, object>();
if (args.ServiceBootstrap != null)
{
payload["bootstrap"] = args.ServiceBootstrap;
}
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", "POST", payload.Count > 0 ? payload : null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service redeployed: {args.ServiceRedeploy}{RESET}");
return;
}
if (args.ServiceExecute != null)
{
var payload = new Dictionary<string, object>
@ -529,6 +559,20 @@ class Un
{
payload["unfreeze_on_demand"] = true;
}
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
@ -1234,10 +1278,12 @@ class Un
public string ServiceShowFreezePage = null;
public bool ServiceShowFreezePageEnabled = true;
public bool ServiceCreateUnfreezeOnDemand = false;
public string ServiceRedeploy = null;
public string EnvFile = null;
public string EnvAction = null;
public string EnvTarget = null;
public bool KeyExtend = false;
public bool LanguagesJson = false;
}
static Args ParseArgs(string[] args)
@ -1249,6 +1295,7 @@ class Un
if (arg == "session") result.Command = "session";
else if (arg == "service") result.Command = "service";
else if (arg == "key") result.Command = "key";
else if (arg == "languages") result.Command = "languages";
else if (arg == "env" && result.Command == "service")
{
// Parse: service env <action> <target>
@ -1295,12 +1342,98 @@ class Un
else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i];
else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
else if (arg == "--redeploy") result.ServiceRedeploy = args[++i];
else if (arg == "--extend") result.KeyExtend = true;
else if (arg == "--json") result.LanguagesJson = true;
else if (!arg.StartsWith("-")) result.SourceFile = arg;
}
return result;
}
static string GetLanguagesCachePath()
{
string home = Environment.GetEnvironmentVariable("HOME")
?? Environment.GetEnvironmentVariable("USERPROFILE")
?? ".";
return Path.Combine(home, ".unsandbox", "languages.json");
}
static List<string> LoadLanguagesCache()
{
string cachePath = GetLanguagesCachePath();
if (!File.Exists(cachePath)) return null;
try
{
string content = File.ReadAllText(cachePath);
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now - mtime < 3600)
{
var data = ParseJson(content);
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString());
}
}
catch { }
return null;
}
static void SaveLanguagesCache(List<string> languages)
{
try
{
string cachePath = GetLanguagesCachePath();
string cacheDir = Path.GetDirectoryName(cachePath);
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sb = new StringBuilder();
sb.Append("{\"languages\":[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
File.WriteAllText(cachePath, sb.ToString());
}
catch { }
}
static void CmdLanguages(Args args)
{
// Try cache first
var languages = LoadLanguagesCache();
if (languages == null)
{
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
var result = ApiRequest("/languages", "GET", null, publicKey, secretKey);
languages = new List<string>();
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
languages = langs.ConvertAll(x => x.ToString());
SaveLanguagesCache(languages);
}
if (args.LanguagesJson)
{
var sb = new StringBuilder("[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("]");
Console.WriteLine(sb.ToString());
}
else
{
foreach (var lang in languages)
Console.WriteLine(lang);
}
}
static void PrintHelp()
{
Console.WriteLine(@"Usage: Un [options] <source_file>
@ -1308,6 +1441,7 @@ class Un
Un service [options]
Un service env <action> <service_id> [options]
Un key [options]
Un languages [--json]
Execute options:
-e KEY=VALUE Set environment variable
@ -1340,6 +1474,7 @@ Service options:
--show-freeze-page-enabled BOOL Enable/disable (default: true)
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
--destroy ID Destroy service
--redeploy ID Re-run bootstrap (with optional --bootstrap, -f)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
@ -1354,7 +1489,10 @@ Service env commands:
env delete ID Delete vault
Key options:
--extend Open browser to extend expired key");
--extend Open browser to extend expired key
Languages options:
--json Output as JSON array");
}
}
@ -1486,15 +1624,76 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
}
/// <summary>Get available programming languages</summary>
private const int LANGUAGES_CACHE_TTL = 3600; // 1 hour
private static string GetLanguagesCachePath()
{
string home = Environment.GetEnvironmentVariable("HOME")
?? Environment.GetEnvironmentVariable("USERPROFILE")
?? ".";
return Path.Combine(home, ".unsandbox", "languages.json");
}
private static List<string> LoadLanguagesCache()
{
string cachePath = GetLanguagesCachePath();
if (!File.Exists(cachePath)) return null;
try
{
string content = File.ReadAllText(cachePath);
double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds();
double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now - mtime < LANGUAGES_CACHE_TTL)
{
var data = ParseJson(content);
if (data.ContainsKey("languages") && data["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString());
}
}
catch { }
return null;
}
private static void SaveLanguagesCache(List<string> languages)
{
try
{
string cachePath = GetLanguagesCachePath();
string cacheDir = Path.GetDirectoryName(cachePath);
if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir);
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var sb = new StringBuilder();
sb.Append("{\"languages\":[");
for (int i = 0; i < languages.Count; i++)
{
if (i > 0) sb.Append(",");
sb.Append("\"").Append(languages[i]).Append("\"");
}
sb.Append("],\"timestamp\":").Append(timestamp).Append("}");
File.WriteAllText(cachePath, sb.ToString());
}
catch { }
}
/// <summary>Get available programming languages (cached for 1 hour)</summary>
public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
{
// Try cache first
var cached = LoadLanguagesCache();
if (cached != null) return cached;
var (pk, sk) = ResolveKeys(publicKey, secretKey);
try
{
var result = ApiCall("/languages", "GET", null, pk, sk);
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
return langs.ConvertAll(x => x.ToString());
{
var languages = langs.ConvertAll(x => x.ToString());
SaveLanguagesCache(languages);
return languages;
}
return new List<string>();
}
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
@ -1632,7 +1831,7 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return null; }
}
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, string publicKey = null, string secretKey = null)
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
{
var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = new Dictionary<string, object> { ["name"] = name };
@ -1645,6 +1844,7 @@ public static class Unsandbox
if (domains != null) payload["domains"] = domains;
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (networkMode != null) payload["network"] = networkMode;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
try
{
var result = ApiCall("/services", "POST", payload, pk, sk);
@ -1696,10 +1896,16 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return false; }
}
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, string publicKey = null, string secretKey = null)
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
{
var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
Dictionary<string, object> payload = null;
if (bootstrap != null || (inputFiles != null && inputFiles.Count > 0))
{
payload = new Dictionary<string, object>();
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
}
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
catch (Exception ex) { _lastError = ex.Message; return false; }
}

View file

@ -392,7 +392,22 @@ void CmdService(Args args)
if (args.ServiceRedeploy != null)
{
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey);
var payload = new Dictionary<string, object>();
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, payload.Count > 0 ? payload : null, publicKey, secretKey);
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
return;
}
@ -452,6 +467,20 @@ void CmdService(Args args)
if (args.Network != null) payload["network"] = args.Network;
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
if (args.Files.Count > 0)
{
var inputFiles = new List<Dictionary<string, string>>();
foreach (var filepath in args.Files)
{
var content = File.ReadAllBytes(filepath);
inputFiles.Add(new Dictionary<string, string>
{
["filename"] = Path.GetFileName(filepath),
["content"] = Convert.ToBase64String(content)
});
}
payload["input_files"] = inputFiles;
}
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
@ -1085,7 +1114,7 @@ Service options:
--lock ID Prevent deletion
--unlock ID Allow deletion
--resize ID Resize (use with -v)
--redeploy ID Re-run bootstrap
--redeploy ID Re-run bootstrap (use -f to include input files)
--snapshot ID Create snapshot from service
--unfreeze-on-demand ID Set unfreeze-on-demand for service
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
@ -1098,6 +1127,7 @@ Service options:
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
-e KEY=VALUE Set vault env var (with --name or env set)
-f FILE Add input file (with --name or --redeploy)
--env-file FILE Load vault vars from file
Service env commands:
@ -1406,7 +1436,7 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return null; }
}
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null)
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
{
var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = new Dictionary<string, object> { ["name"] = name };
@ -1414,6 +1444,7 @@ public static class Unsandbox
if (domains != null) payload["domains"] = domains;
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (networkMode != null) payload["network"] = networkMode;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
try
{
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
@ -1465,10 +1496,16 @@ public static class Unsandbox
catch (Exception ex) { _lastError = ex.Message; return false; }
}
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null)
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
{
var (pk, sk) = ResolveKeys(publicKey, secretKey);
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
Dictionary<string, object>? payload = null;
if (bootstrap != null || inputFiles != null)
{
payload = new Dictionary<string, object>();
if (bootstrap != null) payload["bootstrap"] = bootstrap;
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
}
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
catch (Exception ex) { _lastError = ex.Message; return false; }
}

View file

@ -21,6 +21,7 @@ import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
@ -840,12 +841,19 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int
// Service Operations
// ============================================================================
// InputFile represents a file to upload with a service create or redeploy.
type InputFile struct {
Filename string `json:"filename"`
Content string `json:"content"` // base64-encoded
}
// ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct {
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
NetworkMode string // "zerotrust" (default) or "semitrusted"
Shell string // Shell to use for bootstrap
VCPU int // Number of virtual CPUs
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
InputFiles []InputFile // Files to include (written to /tmp/ in container)
}
// ServiceUpdateOptions contains optional parameters for service updates.
@ -902,6 +910,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin
if opts.UnfreezeOnDemand {
data["unfreeze_on_demand"] = true
}
if len(opts.InputFiles) > 0 {
data["input_files"] = opts.InputFiles
}
}
return makeRequest("POST", "/services", creds, data)
@ -1017,11 +1028,15 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac
// creds: API credentials
// serviceID: Service ID
// bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) {
// inputFiles: Optional files to include (written to /tmp/ in container)
func RedeployService(creds *Credentials, serviceID string, bootstrap string, inputFiles []InputFile) (map[string]interface{}, error) {
data := make(map[string]interface{})
if bootstrap != "" {
data["bootstrap"] = bootstrap
}
if len(inputFiles) > 0 {
data["input_files"] = inputFiles
}
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
}
@ -1899,6 +1914,22 @@ func readFileContents(path string) (string, error) {
return string(data), nil
}
// buildInputFiles reads files from paths and returns InputFile structs with base64-encoded content.
func buildInputFiles(paths []string) ([]InputFile, error) {
var files []InputFile
for _, fpath := range paths {
data, err := os.ReadFile(fpath)
if err != nil {
return nil, fmt.Errorf("cannot read input file %s: %w", fpath, err)
}
files = append(files, InputFile{
Filename: filepath.Base(fpath),
Content: base64.StdEncoding.EncodeToString(data),
})
}
return files, nil
}
// readEnvFile reads environment variables from a .env file
func readEnvFile(path string) (map[string]string, error) {
data, err := os.ReadFile(path)
@ -2353,7 +2384,15 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
// Redeploy service
if fs.redeploy != "" {
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap)
var inputFiles []InputFile
if len(opts.Files) > 0 {
var err error
inputFiles, err = buildInputFiles(opts.Files)
if err != nil {
return cliError(err.Error(), ExitGeneralError)
}
}
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap, inputFiles)
if err != nil {
return cliError(err.Error(), ExitAPIError)
}
@ -2412,6 +2451,13 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
if opts.VCPU > 0 {
serviceOpts.VCPU = opts.VCPU
}
if len(opts.Files) > 0 {
inputFiles, err := buildInputFiles(opts.Files)
if err != nil {
return cliError(err.Error(), ExitGeneralError)
}
serviceOpts.InputFiles = inputFiles
}
service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
if err != nil {

View file

@ -1493,6 +1493,31 @@ public class Un {
String bootstrap,
String publicKey,
String secretKey
) throws IOException {
return createService(name, ports, bootstrap, null, publicKey, secretKey);
}
/**
* Create a new service (long-running container) with optional input files.
*
* @param name Service name
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createService(
String name,
String ports,
String bootstrap,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
@ -1517,6 +1542,9 @@ public class Un {
data.put("bootstrap", bootstrap);
}
}
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
@ -1542,6 +1570,33 @@ public class Un {
boolean unfreezeOnDemand,
String publicKey,
String secretKey
) throws IOException {
return createService(name, ports, bootstrap, unfreezeOnDemand, null, publicKey, secretKey);
}
/**
* Create a new service (long-running container) with unfreeze-on-demand option and input files.
*
* @param name Service name (used for hostname)
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createService(
String name,
String ports,
String bootstrap,
boolean unfreezeOnDemand,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
@ -1569,6 +1624,9 @@ public class Un {
if (unfreezeOnDemand) {
data.put("unfreeze_on_demand", true);
}
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
@ -1901,9 +1959,34 @@ public class Un {
String serviceId,
String publicKey,
String secretKey
) throws IOException {
return redeployService(serviceId, null, publicKey, secretKey);
}
/**
* Redeploy a service (re-run bootstrap script) with optional input files.
*
* @param serviceId Service ID to redeploy
* @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with redeploy confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> redeployService(
String serviceId,
List<Map<String, String>> inputFiles,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
Map<String, Object> data = new LinkedHashMap<>();
if (inputFiles != null && !inputFiles.isEmpty()) {
data.put("input_files", inputFiles);
}
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], data);
}
/**
@ -2859,7 +2942,7 @@ public class Un {
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
break;
case "service":
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files);
break;
case "snapshot":
handleSnapshot(positionalArgs, publicKey, secretKey);
@ -3181,7 +3264,8 @@ public class Un {
String secretKey,
String networkMode,
int vcpu,
List<String> envVars
List<String> envVars,
List<String> files
) throws Exception {
// Check for "env" subcommand
if (args.size() > 1 && args.get(1).equals("env")) {
@ -3347,14 +3431,17 @@ public class Un {
System.err.print(stderr);
}
} else if (redeployId != null) {
redeployService(redeployId, publicKey, secretKey);
// Build input_files from -f args
List<Map<String, String>> inputFiles = buildInputFiles(files);
redeployService(redeployId, inputFiles, publicKey, secretKey);
System.out.println("Service redeployed: " + redeployId);
} else if (snapshotId != null) {
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
System.out.println("Snapshot created: " + snapId);
} else if (name != null) {
// Create new service
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
// Build input_files from -f args
List<Map<String, String>> inputFiles = buildInputFiles(files);
Map<String, Object> result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey);
System.out.println("Service created:");
printMap(result);
} else {
@ -3363,6 +3450,26 @@ public class Un {
}
}
/**
* Build input_files list from -f file paths: read each file, base64-encode, return list of maps.
*/
private static List<Map<String, String>> buildInputFiles(List<String> filePaths) throws IOException {
if (filePaths == null || filePaths.isEmpty()) {
return null;
}
List<Map<String, String>> inputFiles = new ArrayList<>();
for (String fpath : filePaths) {
Path p = Paths.get(fpath);
byte[] content = Files.readAllBytes(p);
String encoded = Base64.getEncoder().encodeToString(content);
Map<String, String> entry = new LinkedHashMap<>();
entry.put("filename", p.getFileName().toString());
entry.put("content", encoded);
inputFiles.add(entry);
}
return inputFiles;
}
private static void handleServiceEnv(
List<String> args,
String publicKey,

View file

@ -1093,6 +1093,7 @@ async function listServices(publicKey, secretKey) {
* - domains: Array of custom domains
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
* - inputFiles: Array of {filename, content} objects (content is base64-encoded)
*
* Returns: Promise<Object> (service info with service_id)
*/
@ -1114,6 +1115,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre
if (opts.domains) data.custom_domains = opts.domains;
if (opts.serviceType) data.service_type = opts.serviceType;
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
if (opts.inputFiles && opts.inputFiles.length > 0) data.input_files = opts.inputFiles;
return makeRequest('POST', '/services', publicKey, secretKey, data);
}
@ -1331,10 +1333,11 @@ async function exportServiceEnv(serviceId, publicKey, secretKey) {
* Args:
* serviceId: Service ID to redeploy
* bootstrap: Optional new bootstrap script content or URL
* inputFiles: Optional array of {filename, content} objects (content is base64-encoded)
*
* Returns: Promise<Object> (redeploy confirmation)
*/
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) {
async function redeployService(serviceId, bootstrap = null, inputFiles = null, publicKey, secretKey) {
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
const data = {};
if (bootstrap) {
@ -1344,6 +1347,7 @@ async function redeployService(serviceId, bootstrap = null, publicKey, secretKey
data.bootstrap_content = bootstrap;
}
}
if (inputFiles && inputFiles.length > 0) data.input_files = inputFiles;
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
}
@ -2146,7 +2150,7 @@ SERVICE COMMANDS:
node un.js service --lock <id> Prevent deletion
node un.js service --unlock <id> Allow deletion
node un.js service --execute <id> <cmd> Run command in service
node un.js service --redeploy <id> Re-run bootstrap
node un.js service --redeploy <id> Re-run bootstrap (supports -f)
node un.js service --snapshot <id> Create snapshot
SERVICE ENV COMMANDS:
@ -2732,7 +2736,19 @@ async function handleService(opts) {
if (opts.bootstrapFile) {
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
}
await redeployService(opts.redeploy, bootstrap, pk, sk);
// Build input_files from -f args
let inputFiles = null;
if (opts.files && opts.files.length > 0) {
inputFiles = [];
for (const fpath of opts.files) {
const content = fs.readFileSync(fpath);
inputFiles.push({
filename: path.basename(fpath),
content: content.toString('base64'),
});
}
}
await redeployService(opts.redeploy, bootstrap, inputFiles, pk, sk);
console.log(`Service ${opts.redeploy} redeployed.`);
return;
}
@ -2786,6 +2802,17 @@ async function handleService(opts) {
if (opts.type) {
serviceOpts.serviceType = opts.type;
}
// Build input_files from -f args
if (opts.files && opts.files.length > 0) {
serviceOpts.inputFiles = [];
for (const fpath of opts.files) {
const content = fs.readFileSync(fpath);
serviceOpts.inputFiles.push({
filename: path.basename(fpath),
content: content.toString('base64'),
});
}
}
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
console.log(`Service created: ${service.service_id}`);

View file

@ -472,6 +472,7 @@ sub service_redeploy {
my ($service_id, %opts) = @_;
my $body = {};
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
$body->{input_files} = $opts{input_files} if $opts{input_files};
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
}
@ -998,7 +999,12 @@ sub cmd_service {
}
if ($options->{redeploy}) {
Un::service_redeploy($options->{redeploy}, bootstrap => $options->{bootstrap});
my %opts;
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
Un::service_redeploy($options->{redeploy}, %opts);
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
return;
}

View file

@ -1339,13 +1339,18 @@ class Unsandbox {
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'input_files'
* @return array Response array with redeploy confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null, array $opts = []): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
$data = [];
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, $data);
}
/**
@ -2603,7 +2608,31 @@ class Unsandbox {
}
if ($serviceOpts['redeploy']) {
$result = $this->redeployService($serviceOpts['redeploy']);
$redeployOpts = [];
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$redeployOpts['input_files'] = $inputFiles;
}
$result = $this->redeployService($serviceOpts['redeploy'], null, null, $redeployOpts);
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
return;
}
@ -2647,6 +2676,31 @@ class Unsandbox {
$createOpts['unfreeze_on_demand'] = true;
}
// Handle input files
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$createOpts['input_files'] = $inputFiles;
}
// Handle bootstrap from file
$bootstrap = $serviceOpts['bootstrap'] ?? '';
if (!empty($serviceOpts['bootstrap_file'])) {
@ -3438,7 +3492,7 @@ SERVICE OPTIONS:
--lock ID Prevent service deletion
--unlock ID Allow service deletion
--resize ID Resize service (with --vcpu)
--redeploy ID Re-run bootstrap
--redeploy ID Re-run bootstrap (supports -f/-F for input files)
--execute ID 'cmd' Execute command in service
--snapshot ID Create service snapshot

View file

@ -57,6 +57,7 @@ require 'openssl'
require 'fileutils'
require 'optparse'
require 'cgi'
require 'base64'
# Unsandbox Ruby SDK module (synchronous)
module Un
@ -922,6 +923,7 @@ module Un
# @param custom_domains [Array<String>, nil] Custom domains for the service
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
# @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @return [Hash] Response hash with service_id
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
@ -929,7 +931,7 @@ module Un
# @example
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
# puts result["service_id"]
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false)
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false, input_files: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {
name: name,
@ -941,6 +943,7 @@ module Un
data[:custom_domains] = custom_domains if custom_domains
data[:service_type] = service_type if service_type
data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand
data[:input_files] = input_files if input_files && !input_files.empty?
make_request('POST', '/services', pk, sk, data)
end
@ -1202,16 +1205,18 @@ module Un
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param bootstrap [String, nil] New bootstrap script (optional)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @return [Hash] Response hash with redeploy confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.redeploy_service(service_id)
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil)
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil, input_files: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:bootstrap] = bootstrap if bootstrap
data[:input_files] = input_files if input_files && !input_files.empty?
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
end
@ -2420,7 +2425,17 @@ module Un
elsif service_opts[:bootstrap]
bootstrap = service_opts[:bootstrap]
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds)
# Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, input_files: service_input_files, **creds)
puts "Service #{service_opts[:redeploy]} redeployed"
elsif service_opts[:execute]
cmd = service_opts[:execute_cmd]
@ -2453,6 +2468,17 @@ module Un
exit(EXIT_INVALID_ARGS)
end
# Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
end
result = create_service(
service_opts[:name],
service_opts[:ports],
@ -2461,6 +2487,7 @@ module Un
vcpu: options[:vcpu],
custom_domains: service_opts[:domains],
service_type: service_opts[:type],
input_files: service_input_files,
**creds
)
puts "Service created: #{result['service_id']}"

View file

@ -32,6 +32,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Base64 encoding (for input_files)
base64 = "0.22"
# HMAC-SHA256 authentication
hmac = "0.12"
sha2 = "0.10"

View file

@ -58,6 +58,7 @@
// - TTL: 1 hour
// - Updated on successful API calls
use base64::Engine;
use hmac::{Hmac, Mac};
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
@ -382,6 +383,17 @@ pub struct ServiceCreateOptions {
pub bootstrap_url: Option<String>,
/// Whether to enable automatic unfreezing on incoming HTTP requests
pub unfreeze_on_demand: Option<bool>,
/// Input files to upload (filename + base64-encoded content)
pub input_files: Option<Vec<InputFile>>,
}
/// A file to upload with a service create or redeploy request
#[derive(Debug, Clone, Serialize)]
pub struct InputFile {
/// Filename (basename only)
pub filename: String,
/// Base64-encoded file content
pub content: String,
}
/// Options for updating a service
@ -1997,6 +2009,11 @@ pub fn create_service(
if let Some(unfreeze_on_demand) = opts.unfreeze_on_demand {
body["unfreeze_on_demand"] = serde_json::json!(unfreeze_on_demand);
}
if let Some(input_files) = opts.input_files {
if !input_files.is_empty() {
body["input_files"] = serde_json::json!(input_files);
}
}
}
make_request("POST", "/services", creds, Some(&body))
@ -2269,12 +2286,22 @@ pub fn export_service_env(service_id: &str, creds: &Credentials) -> Result<Strin
/// # Arguments
/// * `service_id` - Service ID to redeploy
/// * `creds` - API credentials
/// * `input_files` - Optional files to upload (filename + base64-encoded content)
///
/// # Returns
/// Updated Service information
pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service> {
pub fn redeploy_service(
service_id: &str,
creds: &Credentials,
input_files: Option<Vec<InputFile>>,
) -> Result<Service> {
let path = format!("/services/{}/redeploy", service_id);
let body = serde_json::json!({});
let mut body = serde_json::json!({});
if let Some(files) = input_files {
if !files.is_empty() {
body["input_files"] = serde_json::json!(files);
}
}
make_request("POST", &path, creds, Some(&body))
}
@ -2861,7 +2888,7 @@ SESSION COMMANDS:
SERVICE COMMANDS:
un service --list List all services
un service --name NAME --ports P Create service
un service --name NAME --ports P [-f FILE] Create service
un service --info ID Get service details
un service --logs ID Get all logs
un service --tail ID Get last 9000 lines
@ -2871,7 +2898,7 @@ SERVICE COMMANDS:
un service --lock ID Prevent deletion
un service --unlock ID Allow deletion
un service --execute ID 'cmd' Run command
un service --redeploy ID Re-run bootstrap
un service --redeploy ID [-f FILE] Re-run bootstrap
un service --snapshot ID Create snapshot
un service env status ID Show vault status
un service env set ID Set env vars
@ -3586,6 +3613,33 @@ fn cmd_session(opts: &CliOptions) -> i32 {
}
}
/// Build input_files from -f file paths: read each file, base64 encode, return as InputFile vec.
fn build_input_files(files: &[String]) -> Option<Vec<InputFile>> {
if files.is_empty() {
return None;
}
let engine = base64::engine::general_purpose::STANDARD;
let mut input_files = Vec::new();
for fpath in files {
let path = std::path::Path::new(fpath);
match fs::read(path) {
Ok(data) => {
let filename = path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| fpath.clone());
input_files.push(InputFile {
filename,
content: engine.encode(&data),
});
}
Err(e) => {
eprintln!("Warning: Cannot read file '{}': {}", fpath, e);
}
}
}
if input_files.is_empty() { None } else { Some(input_files) }
}
fn cmd_service(opts: &CliOptions) -> i32 {
let creds = match get_credentials(opts) {
Ok(c) => c,
@ -3736,7 +3790,9 @@ fn cmd_service(opts: &CliOptions) -> i32 {
}
if let Some(ref id) = opts.service_redeploy {
match redeploy_service(id, &creds) {
// Build input_files from -f args
let service_input_files = build_input_files(&opts.files);
match redeploy_service(id, &creds, service_input_files) {
Ok(service) => {
println!("Service {} redeployed.", service.service_id);
return EXIT_SUCCESS;
@ -3809,12 +3865,16 @@ fn cmd_service(opts: &CliOptions) -> i32 {
String::new()
};
// Build input_files from -f args
let service_input_files = build_input_files(&opts.files);
let service_opts = ServiceCreateOptions {
network_mode: opts.network.clone(),
vcpu: opts.vcpu,
domains: opts.service_domains.as_ref().map(|d| {
d.split(',').map(|s| s.trim().to_string()).collect()
}),
input_files: service_input_files,
..Default::default()
};

View file

@ -195,6 +195,7 @@ interface Args {
setUnfreezeOnDemand: string | null;
showFreezePage: boolean | null;
setShowFreezePage: string | null;
redeploy: string | null;
}
interface ApiKeys {
@ -897,6 +898,37 @@ async function cmdService(args: Args): Promise<void> {
return;
}
if (args.redeploy) {
const payload: any = {};
if (args.bootstrap) {
payload.bootstrap = args.bootstrap;
}
if (args.bootstrapFile) {
if (!fs.existsSync(args.bootstrapFile)) {
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
process.exit(1);
}
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
}
if (args.files && args.files.length > 0) {
payload.input_files = args.files.map(filepath => {
try {
const content = fs.readFileSync(filepath);
return {
filename: path.basename(filepath),
content_base64: content.toString('base64')
};
} catch (e) {
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
process.exit(1);
}
});
}
await apiRequest(`/services/${args.redeploy}/redeploy`, "POST", payload, keys);
console.log(`${GREEN}Service redeployed: ${args.redeploy}${RESET}`);
return;
}
if (args.execute) {
const payload = { command: args.command_arg };
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
@ -1225,6 +1257,7 @@ function parseArgs(argv: string[]): Args {
setUnfreezeOnDemand: null,
showFreezePage: null,
setShowFreezePage: null,
redeploy: null,
};
let i = 2;
@ -1397,6 +1430,9 @@ function parseArgs(argv: string[]): Args {
} else if (arg === '--set-show-freeze-page' && i + 1 < argv.length) {
args.setShowFreezePage = argv[++i];
i++;
} else if (arg === '--redeploy' && i + 1 < argv.length) {
args.redeploy = argv[++i];
i++;
} else if (!arg.startsWith('-')) {
args.sourceFile = arg;
i++;
@ -1474,6 +1510,7 @@ Service options:
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires -v)
--redeploy ID Re-run bootstrap (with optional --bootstrap, --bootstrap-file, -f)
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script