feat(vm): add vApp + VM terraform example
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// Создано: 2026-04-10
|
||||
// Простой калькулятор — возвращает HTML страницу с кнопками.
|
||||
// GET → HTML; POST с {a, op, b} → вычисление с результатом на странице.
|
||||
// Рантайм nodejs20 v0.1.3+ поддерживает HTML-ответ (строка начинающаяся с '<').
|
||||
|
||||
'use strict';
|
||||
|
||||
const HTML = `<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Калькулятор (Node.js)</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0f2027; }
|
||||
.calc { background: #1a2a1a; border-radius: 16px; padding: 24px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); width: 280px; }
|
||||
h2 { color: #d4f7d4; text-align: center; margin: 0 0 16px; font-size: 18px; }
|
||||
.badge { text-align: center; font-size: 11px; color: #4a7a4a; margin-bottom: 16px; }
|
||||
.display { background: #0a1a0a; color: #e0ffe0; font-size: 28px; text-align: right; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; min-height: 52px; word-break: break-all; }
|
||||
.result { color: #4ade80; font-size: 20px; text-align: right; padding: 4px 16px; margin-bottom: 8px; min-height: 28px; }
|
||||
.btns { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
||||
button { border: none; border-radius: 8px; padding: 16px 0; font-size: 18px; cursor: pointer; transition: filter 0.1s; }
|
||||
button:active { filter: brightness(1.3); }
|
||||
.btn-num { background: #1e3a1e; color: #d4f7d4; }
|
||||
.btn-op { background: #166534; color: #fff; }
|
||||
.btn-eq { background: #15803d; color: #fff; grid-column: span 2; }
|
||||
.btn-clr { background: #7f1d1d; color: #fca5a5; grid-column: span 2; }
|
||||
.btn-zero { grid-column: span 2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calc">
|
||||
<h2>Калькулятор</h2>
|
||||
<div class="badge">Node.js 20</div>
|
||||
<div id="display" class="display">0</div>
|
||||
<div id="result" class="result">RESULT_LINE</div>
|
||||
<div class="btns">
|
||||
<button class="btn-clr btn-zero" onclick="clr()">C</button>
|
||||
<button class="btn-op" onclick="setOp('%2F')">÷</button>
|
||||
<button class="btn-op" onclick="setOp('*')">×</button>
|
||||
<button class="btn-num" onclick="inp('7')">7</button>
|
||||
<button class="btn-num" onclick="inp('8')">8</button>
|
||||
<button class="btn-num" onclick="inp('9')">9</button>
|
||||
<button class="btn-op" onclick="setOp('-')">−</button>
|
||||
<button class="btn-num" onclick="inp('4')">4</button>
|
||||
<button class="btn-num" onclick="inp('5')">5</button>
|
||||
<button class="btn-num" onclick="inp('6')">6</button>
|
||||
<button class="btn-op" onclick="setOp('+')">+</button>
|
||||
<button class="btn-num" onclick="inp('1')">1</button>
|
||||
<button class="btn-num" onclick="inp('2')">2</button>
|
||||
<button class="btn-num" onclick="inp('3')">3</button>
|
||||
<button class="btn-num btn-zero" onclick="inp('0')">0</button>
|
||||
<button class="btn-num" onclick="inp('.')">.</button>
|
||||
<button class="btn-eq" onclick="calc()">=</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let a = '', op = '', b = '';
|
||||
const disp = document.getElementById('display');
|
||||
function inp(v) {
|
||||
if (op === '') { a += v; disp.textContent = a || '0'; }
|
||||
else { b += v; disp.textContent = b || '0'; }
|
||||
}
|
||||
function setOp(o) {
|
||||
if (a === '') return;
|
||||
if (b !== '') calc();
|
||||
else { op = o; disp.textContent = a + ' ' + decodeURIComponent(o) + ' …'; }
|
||||
}
|
||||
function clr() { a = ''; op = ''; b = ''; disp.textContent = '0'; }
|
||||
function calc() {
|
||||
if (a === '' || op === '' || b === '') return;
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST'; form.action = '';
|
||||
const fields = {a, op, b};
|
||||
for (const [k, v] of Object.entries(fields)) {
|
||||
const i = document.createElement('input');
|
||||
i.type = 'hidden'; i.name = k; i.value = decodeURIComponent(v);
|
||||
form.appendChild(i);
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Форматирует число — убирает лишний .0 для целых
|
||||
function fmt(n) { return n === Math.trunc(n) ? String(Math.trunc(n)) : String(n); }
|
||||
|
||||
function buildPage(resultLine) {
|
||||
return HTML.replace('RESULT_LINE', resultLine || '');
|
||||
}
|
||||
|
||||
function compute(event) {
|
||||
// POST: тело приходит как JSON; поля a, op, b — строки из form
|
||||
const a = parseFloat(event.a);
|
||||
const b = parseFloat(event.b);
|
||||
const op = event.op;
|
||||
if (isNaN(a) || isNaN(b)) return buildPage('Некорректное число');
|
||||
let res;
|
||||
switch (op) {
|
||||
case '+': res = a + b; break;
|
||||
case '-': res = a - b; break;
|
||||
case '*': res = a * b; break;
|
||||
case '/':
|
||||
if (b === 0) return buildPage('Деление на ноль');
|
||||
res = a / b; break;
|
||||
default: return buildPage('Неизвестный оператор');
|
||||
}
|
||||
return buildPage(`${fmt(a)} ${op} ${fmt(b)} = ${fmt(res)}`);
|
||||
}
|
||||
|
||||
module.exports.handler = async function handler(event) {
|
||||
if (event._method === 'POST') return compute(event);
|
||||
return buildPage('');
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# Создано: 2026-04-10
|
||||
# Простой калькулятор — возвращает HTML страницу с кнопками.
|
||||
# Роутинг: GET → HTML страница; POST с {a, op, b} → вычисление и редирект с результатом.
|
||||
# _method приходит от python3.11 рантайма в event.
|
||||
|
||||
_HTML = """<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Калькулятор (Python)</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #1a1a2e; }
|
||||
.calc { background: #16213e; border-radius: 16px; padding: 24px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); width: 280px; }
|
||||
h2 { color: #e2e8f0; text-align: center; margin: 0 0 16px; font-size: 18px; }
|
||||
.badge { text-align: center; font-size: 11px; color: #64748b; margin-bottom: 16px; }
|
||||
.display { background: #0f172a; color: #f1f5f9; font-size: 28px; text-align: right; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; min-height: 52px; word-break: break-all; }
|
||||
.result { color: #22d3ee; font-size: 20px; text-align: right; padding: 4px 16px; margin-bottom: 8px; min-height: 28px; }
|
||||
.btns { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
||||
button { border: none; border-radius: 8px; padding: 16px 0; font-size: 18px; cursor: pointer; transition: filter 0.1s; }
|
||||
button:active { filter: brightness(1.3); }
|
||||
.btn-num { background: #334155; color: #f1f5f9; }
|
||||
.btn-op { background: #0891b2; color: #fff; }
|
||||
.btn-eq { background: #0d9488; color: #fff; grid-column: span 2; }
|
||||
.btn-clr { background: #7f1d1d; color: #fca5a5; grid-column: span 2; }
|
||||
.btn-zero { grid-column: span 2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calc">
|
||||
<h2>Калькулятор</h2>
|
||||
<div class="badge">Python 3.11</div>
|
||||
<div id="display" class="display">0</div>
|
||||
<div id="result" class="result">{result_line}</div>
|
||||
<div class="btns">
|
||||
<button class="btn-clr btn-zero" onclick="clr()">C</button>
|
||||
<button class="btn-op" onclick="setOp('%2F')">÷</button>
|
||||
<button class="btn-op" onclick="setOp('*')">×</button>
|
||||
<button class="btn-num" onclick="inp('7')">7</button>
|
||||
<button class="btn-num" onclick="inp('8')">8</button>
|
||||
<button class="btn-num" onclick="inp('9')">9</button>
|
||||
<button class="btn-op" onclick="setOp('-')">−</button>
|
||||
<button class="btn-num" onclick="inp('4')">4</button>
|
||||
<button class="btn-num" onclick="inp('5')">5</button>
|
||||
<button class="btn-num" onclick="inp('6')">6</button>
|
||||
<button class="btn-op" onclick="setOp('+')">+</button>
|
||||
<button class="btn-num" onclick="inp('1')">1</button>
|
||||
<button class="btn-num" onclick="inp('2')">2</button>
|
||||
<button class="btn-num" onclick="inp('3')">3</button>
|
||||
<button class="btn-num btn-zero" onclick="inp('0')">0</button>
|
||||
<button class="btn-num" onclick="inp('.')">.</button>
|
||||
<button class="btn-eq" onclick="calc()">=</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let a = '', op = '', b = '', fresh = {fresh_js};
|
||||
const disp = document.getElementById('display');
|
||||
function inp(v) {{
|
||||
if (op === '') {{ a += v; disp.textContent = a || '0'; }}
|
||||
else {{ b += v; disp.textContent = b || '0'; }}
|
||||
fresh = false;
|
||||
}}
|
||||
function setOp(o) {{
|
||||
if (a === '') return;
|
||||
if (b !== '') calc();
|
||||
else {{ op = o; disp.textContent = a + ' ' + decodeURIComponent(o) + ' …'; }}
|
||||
fresh = false;
|
||||
}}
|
||||
function clr() {{ a = ''; op = ''; b = ''; disp.textContent = '0'; }}
|
||||
function calc() {{
|
||||
if (a === '' || op === '' || b === '') return;
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST'; form.action = '';
|
||||
const fields = {{a, op, b}};
|
||||
for (const [k, v] of Object.entries(fields)) {{
|
||||
const i = document.createElement('input');
|
||||
i.type = 'hidden'; i.name = k; i.value = decodeURIComponent(v);
|
||||
form.appendChild(i);
|
||||
}}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def handler(event):
|
||||
# POST: тело приходит как JSON от рантайма
|
||||
if event.get('_method') == 'POST':
|
||||
return _compute(event)
|
||||
# GET: показываем чистую страницу
|
||||
return _html()
|
||||
|
||||
|
||||
def _html(result_line='', fresh_js='true'):
|
||||
return _HTML.replace('{result_line}', result_line).replace('{fresh_js}', fresh_js)
|
||||
|
||||
|
||||
def _compute(event):
|
||||
try:
|
||||
a = float(event.get('a', 0))
|
||||
b = float(event.get('b', 0))
|
||||
op = event.get('op', '')
|
||||
if op == '+':
|
||||
res = a + b
|
||||
elif op == '-':
|
||||
res = a - b
|
||||
elif op == '*':
|
||||
res = a * b
|
||||
elif op == '/':
|
||||
if b == 0:
|
||||
return _html('Деление на ноль', 'false')
|
||||
res = a / b
|
||||
else:
|
||||
return _html('Неизвестный оператор', 'false')
|
||||
# Убираем лишний .0 для целых результатов
|
||||
res_str = str(int(res)) if res == int(res) else str(res)
|
||||
return _html(f'{int(a) if a == int(a) else a} {op} {int(b) if b == int(b) else b} = {res_str}', 'false')
|
||||
except (ValueError, TypeError) as exc:
|
||||
return _html(f'Ошибка: {exc}', 'false')
|
||||
@@ -0,0 +1 @@
|
||||
# нет внешних зависимостей
|
||||
@@ -0,0 +1,32 @@
|
||||
# Создано: 2026-04-10
|
||||
# functions.tf — sless_service ресурсы для примера POSTGRES.
|
||||
# Здесь: два калькуляторa — Python и Node.js.
|
||||
# sless_service = long-running Deployment + постоянный URL (в отличие от sless_function).
|
||||
|
||||
# ─── Python-калькулятор ──────────────────────────────────────────────────────
|
||||
|
||||
resource "sless_service" "calc_python" {
|
||||
name = "calc-python"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "handler.handler"
|
||||
source_dir = "${path.module}/code/calc-python"
|
||||
}
|
||||
|
||||
output "calc_python_url" {
|
||||
description = "URL Python-калькулятора"
|
||||
value = sless_service.calc_python.url
|
||||
}
|
||||
|
||||
# ─── Node.js-калькулятор ─────────────────────────────────────────────────────
|
||||
|
||||
resource "sless_service" "calc_node" {
|
||||
name = "calc-node"
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "handler.handler"
|
||||
source_dir = "${path.module}/code/calc-node"
|
||||
}
|
||||
|
||||
output "calc_node_url" {
|
||||
description = "URL Node.js-калькулятора"
|
||||
value = sless_service.calc_node.url
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Пример: Виртуальная машина (vApp + VM) в Nubes vDC
|
||||
|
||||
Создаёт:
|
||||
- **vApp** — виртуальный каталог (контейнер для ВМ в VMware vDC)
|
||||
- **ВМ** — Ubuntu 22.04, 2 CPU / 2 GB RAM / 20 GB disk
|
||||
|
||||
---
|
||||
|
||||
## Что нужно сделать перед запуском
|
||||
|
||||
### 1. Сгенерировать SSH-ключ
|
||||
|
||||
Публичный ключ прописывается в ВМ при создании — это единственный способ зайти по SSH.
|
||||
Приватный ключ нужен хранить у себя.
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/sless-demo-vm -N "" -C "sless-demo-vm"
|
||||
```
|
||||
|
||||
Публичный ключ (`~/.ssh/sless-demo-vm.pub`) — строка вида:
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... sless-demo-vm
|
||||
```
|
||||
|
||||
### 2. Заполнить terraform.tfvars
|
||||
|
||||
Открыть файл `terraform.tfvars` и заменить значения:
|
||||
|
||||
```hcl
|
||||
# Ваш API-токен из панели Nubes
|
||||
api_token = "ВСТАВИТЬ_ТОКЕН"
|
||||
|
||||
# Публичный ключ из шага 1
|
||||
vm_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
|
||||
```
|
||||
|
||||
> **Токен** — берётся в панели Nubes: профиль → API-токены.
|
||||
> **Ключ** — содержимое файла `~/.ssh/sless-demo-vm.pub` (публичный, не приватный!).
|
||||
|
||||
---
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
cd examples/VM
|
||||
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
После `apply` в выводе будет:
|
||||
|
||||
```
|
||||
Outputs:
|
||||
|
||||
vm_id = "..."
|
||||
vm_state = {
|
||||
"externalIp" = "1.2.3.4"
|
||||
...
|
||||
}
|
||||
vapp_id = "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Подключение по SSH
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/sless-demo-vm ubuntu@<externalIp из outputs>
|
||||
```
|
||||
|
||||
Логин — `ubuntu` (задан в `vm.tf`).
|
||||
|
||||
---
|
||||
|
||||
## Удаление
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
|
||||
Порядок автоматический: сначала suspend → потом delete. Без suspend удаление упадёт с ошибкой — это поведение Nubes, параметр `suspend_on_destroy = true` в ресурсах решает это.
|
||||
|
||||
---
|
||||
|
||||
## Что можно менять
|
||||
|
||||
| Параметр | Файл | Примечание |
|
||||
|----------|------|-----------|
|
||||
| `vm_cpu`, `vm_ram`, `vm_disk` | `vm.tf` | Можно менять и переприменять |
|
||||
| `resource_name`, `vapp_name` | `vapp.tf` | **Не изменяется после создания** |
|
||||
| `image_vm`, `user_login`, `user_public_key` | `vm.tf` | **Не изменяется после создания** |
|
||||
| `vdc_uid`, `nsxt_uid` | `vapp.tf` | **Не изменяется после создания** |
|
||||
@@ -0,0 +1,38 @@
|
||||
// 2026-03-25 — main.tf для примера с vApp + ВМ (Виртуальный датацентр Nubes).
|
||||
// Провайдер nubes. Sless-провайдер не нужен — пример чисто инфраструктурный.
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "terra.k8c.ru/nubes/nubes"
|
||||
version = "5.0.31"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Переменные
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
variable "vm_public_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "Публичный SSH-ключ для ВМ. Приватный ключ: ~/terra/sless/examples/VM/vm_key"
|
||||
}
|
||||
|
||||
variable "api_token" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "Nubes API token"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Провайдер
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# API Dashboard (для Terraform-провайдеров): https://deck-api-test.ngcloud.ru/api/v1/index.cfm
|
||||
# UI облака (только браузер): https://deck-test.ngcloud.ru/
|
||||
provider "nubes" {
|
||||
api_token = var.api_token
|
||||
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 2026-03-25 — vapp.tf: виртуальный каталог ВМ (vApp) в Nubes vDC.
|
||||
// nubes_vapp — контейнер для ВМ внутри Виртуального датацентра.
|
||||
// Обязательные поля: vdc_uid, nsxt_uid, vapp_name, resource_name.
|
||||
|
||||
resource "nubes_vapp" "vapp" {
|
||||
resource_name = "vm-sless-demo-vapp"
|
||||
vapp_name = "vapp-sless-demo" # Уникальное в рамках организации. Не изменяется после создания.
|
||||
vdc_uid = "e3c9e4f1-24da-4992-a003-f8a2a803a5f0" # UUID Услуги «Виртуальный датацентр (vDC)». Не изменяется после создания.
|
||||
nsxt_uid = "0fe88e2a-31b6-4385-ad52-e27c6c0d38a6" # UUID Услуги «Сетевой шлюз периметра (Edge)». Не изменяется после создания.
|
||||
|
||||
adopt_existing_on_create = true
|
||||
operation_timeout = "15m"
|
||||
|
||||
# ВАЖНО: delete без предварительного suspend завершается ошибкой
|
||||
# "Невозможно выполнить операцию удаления услуги. Услуга не остановлена"
|
||||
# suspend_on_destroy гарантирует правильный порядок: suspend → delete.
|
||||
suspend_on_destroy = true
|
||||
}
|
||||
|
||||
output "vapp_id" {
|
||||
value = nubes_vapp.vapp.id
|
||||
description = "ID созданного vApp (используется как vapp_uid при создании ВМ)"
|
||||
}
|
||||
|
||||
output "vapp_state" {
|
||||
value = nubes_vapp.vapp.state_out_flat
|
||||
description = "Плоский state vApp — адреса, статусы сети и т.д."
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 2026-03-25 — vm.tf: виртуальная машина (nubes_vc_vm_v3) внутри vApp.
|
||||
// Зависит от nubes_vapp.vapp — создаётся после vApp.
|
||||
// image_vm, vapp_uid, user_public_key не изменяются после создания.
|
||||
|
||||
resource "nubes_vc_vm_v3" "vm" {
|
||||
resource_name = "vm-sless-demo"
|
||||
|
||||
vapp_uid = nubes_vapp.vapp.id # ссылка на vApp. Не изменяется после создания.
|
||||
image_vm = "Ubuntu 22.04 LTS" # Не изменяется после создания.
|
||||
ip_space_name = "internet-ipv4-v1"
|
||||
|
||||
user_login = "ubuntu"
|
||||
user_public_key = var.vm_public_key # задаётся в terraform.tfvars
|
||||
|
||||
vm_cpu = 2
|
||||
vm_ram = 2 # GB
|
||||
vm_disk = 20 # GB
|
||||
|
||||
adopt_existing_on_create = true
|
||||
operation_timeout = "15m"
|
||||
|
||||
# delete без предварительного suspend завершается ошибкой (аналогично vApp).
|
||||
suspend_on_destroy = true
|
||||
}
|
||||
|
||||
output "vm_id" {
|
||||
value = nubes_vc_vm_v3.vm.id
|
||||
description = "ID созданной ВМ"
|
||||
}
|
||||
|
||||
output "vm_state" {
|
||||
value = nubes_vc_vm_v3.vm.state_out_flat
|
||||
description = "Плоский state ВМ — IP-адреса, статус и т.д."
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Изменено: 2026-03-09
|
||||
// Изменено: 2026-04-10 (v0.1.3) — sendResult: если handler вернул строку начинающуюся с '<' → text/html.
|
||||
// HTTP-обёртка для serverless функций на Node.js 20.
|
||||
// Загружает модуль из SLESS_ENTRYPOINT или handler.js по умолчанию.
|
||||
// Формат SLESS_ENTRYPOINT: "module-name.functionName" (например: handler-http.handle)
|
||||
@@ -65,7 +66,7 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await userHandle(event);
|
||||
sendJSON(res, 200, result);
|
||||
sendResult(res, 200, result);
|
||||
} catch (err) {
|
||||
console.error('Handler error:', err);
|
||||
sendJSON(res, 500, { error: err.message });
|
||||
@@ -73,6 +74,21 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
function sendResult(res, status, data) {
|
||||
// Если handler вернул строку начинающуюся с '<' — HTML страница.
|
||||
// Если строка, но не HTML — text/plain. Иначе — JSON.
|
||||
if (typeof data === 'string') {
|
||||
const ctype = data.trimStart().startsWith('<')
|
||||
? 'text/html; charset=utf-8'
|
||||
: 'text/plain; charset=utf-8';
|
||||
const buf = Buffer.from(data, 'utf-8');
|
||||
res.writeHead(status, { 'Content-Type': ctype, 'Content-Length': buf.length });
|
||||
res.end(buf);
|
||||
return;
|
||||
}
|
||||
sendJSON(res, status, data);
|
||||
}
|
||||
|
||||
function sendJSON(res, status, data) {
|
||||
const body = JSON.stringify(data);
|
||||
res.writeHead(status, {
|
||||
|
||||
Reference in New Issue
Block a user