mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(oci): end a pasted definition with a single Ctrl+D
This commit is contained in:
@@ -1207,7 +1207,7 @@ export function HostBackup() {
|
||||
{!j.attached && j.next_run && (
|
||||
<span className="inline-flex items-center gap-1" title={t("backup.jobs.nextRunTitle")}>
|
||||
<Clock className="h-3.5 w-3.5 text-green-500" />
|
||||
<span>{t("backup.jobs.nextRun", { time: formatNext(j.next_run) })}</span>
|
||||
<span>{t("backup.jobs.nextRun")}: {formatNext(j.next_run)}</span>
|
||||
</span>
|
||||
)}
|
||||
{(statusBadge || lastRunWhen) && (
|
||||
|
||||
@@ -4016,7 +4016,7 @@
|
||||
},
|
||||
"backends": {
|
||||
"borgDescription": "Repositorio Borg para backups del host deduplicados.",
|
||||
"borgDescriptionTimerOnly": "Los destinos Borg son utilizados por los temporizadores Monitor, no por los tareas PVE vzdump.",
|
||||
"borgDescriptionTimerOnly": "Los destinos Borg los usan los temporizadores del Monitor, no las tareas vzdump de PVE.",
|
||||
"borgShortDescription": "Repositorio Borg deduplicado",
|
||||
"local": "Local",
|
||||
"localDescription": "Carpeta en este host o en una unidad USB montada.",
|
||||
@@ -4100,10 +4100,10 @@
|
||||
"localUsbDescription": "Carpeta en este host o una unidad USB montada.",
|
||||
"managedByPve": "Gestionado por PVE",
|
||||
"manuallyAdded": "Agregado manualmente",
|
||||
"nameCredentialsLocked": "El nombre y las credenciales están bloqueados mientras los tareas utilizan este destino.",
|
||||
"namePassphraseLocked": "El nombre del repositorio y la frase de contraseña están bloqueados mientras los tareas utilizan este destino.",
|
||||
"nameCredentialsLocked": "El nombre y las credenciales están bloqueados mientras haya tareas que usen este destino.",
|
||||
"namePassphraseLocked": "El nombre del repositorio y la frase de contraseña están bloqueados mientras haya tareas que usen este destino.",
|
||||
"noBorgInDialog": "La edición de destinos de Borg está disponible desde el flujo de configuración de Borg.",
|
||||
"noBorgInWizard": "Borg no está disponible para tareas adjuntos PVE.",
|
||||
"noBorgInWizard": "Borg no está disponible para las tareas vinculadas a PVE.",
|
||||
"noPbsInDialog": "La edición de destino de PBS está disponible desde el flujo de configuración de PBS.",
|
||||
"noPbsInWizard": "Aún no se han configurado repositorios de PBS.",
|
||||
"not": "no",
|
||||
@@ -4141,7 +4141,7 @@
|
||||
"autoDetectedFromPveStorage": "Detectado desde el almacenamiento PVE",
|
||||
"borgPassphraseLossBefore": "Si pierde la frase de contraseña de Borg, los backups cifrados no se podrán restaurar.",
|
||||
"borgPassphraseOnlyWay": "Esta frase de contraseña es la única forma de desbloquear el repositorio.",
|
||||
"borgPassphraseSavedAfter": "para que los tareas programados puedan ejecutarse sin supervisión.",
|
||||
"borgPassphraseSavedAfter": "para que las tareas programadas puedan ejecutarse sin supervisión.",
|
||||
"borgPassphraseSavedAt": "La frase de contraseña se guarda en",
|
||||
"encryptBackups": "Cifrar backups",
|
||||
"encryptThisBackup": "Cifrar este backup",
|
||||
@@ -4251,7 +4251,7 @@
|
||||
"pathLabel": "Ruta",
|
||||
"pbsRepository": "repositorio de PBS",
|
||||
"profileLabel": "Perfil",
|
||||
"pveJobLabel": "tarea PvE",
|
||||
"pveJobLabel": "Tarea PVE",
|
||||
"pveStorage": "almacenamiento PVE",
|
||||
"pveVersion": "versión PvE",
|
||||
"recoveryPassphrase": "Frase de contraseña de recuperación",
|
||||
@@ -4288,20 +4288,20 @@
|
||||
"sshPort": "Puerto SSH"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "backups.",
|
||||
"attachDescriptionBefore": "Une Monitor a una tarea PVE vzdump y ejecútela después del",
|
||||
"attachDescriptionMiddle": "evento para seleccionados",
|
||||
"attachDescriptionAfter": "seleccionados.",
|
||||
"attachDescriptionBefore": "Vincula el Monitor a una tarea vzdump de PVE y se ejecuta tras el evento",
|
||||
"attachDescriptionMiddle": "para los backups de",
|
||||
"attachToPveJob": "Unirse a una tarea de Backup en PVE",
|
||||
"attachedToPveVzdump": "Adjunto a PVE vzdump",
|
||||
"backendEditHelp": "El backend no se puede cambiar una vez creado la tarea.",
|
||||
"createJob": "crear tarea",
|
||||
"createScheduledJob": "Crear tarea programado",
|
||||
"backendEditHelp": "El backend no se puede cambiar una vez creada la tarea.",
|
||||
"createJob": "Crear tarea",
|
||||
"createScheduledJob": "Crear tarea programada",
|
||||
"customProfileTitle": "Perfil personalizado",
|
||||
"defaultProfileTitle": "Perfil predeterminado",
|
||||
"detailDescription": "Programación, perfil, destino y última ejecución de esta tarea.",
|
||||
"disableJob": "Desactivar tarea",
|
||||
"disableJobDescription": "El temporizador systemd se detendrá; no se ejecutarán más automáticamente. Puede volver a habilitarlo más tarde desde este mismo cuadro de diálogo.",
|
||||
"editScheduledJob": "Editar tarea programado",
|
||||
"editScheduledJob": "Editar tarea programada",
|
||||
"encryptedTitle": "cifrado",
|
||||
"inheritedRetentionLabel": "Retención de la tarea PVE",
|
||||
"inheritedScheduleLabel": "Programación de la tarea PVE",
|
||||
@@ -4338,7 +4338,7 @@
|
||||
"runNowTitle": "Activa esta tarea ahora",
|
||||
"running": "Ejecutando...",
|
||||
"scheduledTitle": "Programado",
|
||||
"standaloneScheduledJob": "Tarea programado independiente",
|
||||
"standaloneScheduledJob": "Tarea programada independiente",
|
||||
"starting": "Iniciando...",
|
||||
"stepOf": "Paso {step} de {total}",
|
||||
"summary": "Resumen",
|
||||
@@ -4592,7 +4592,7 @@
|
||||
"advancedHelpBefore": "Usar",
|
||||
"advancedHelpMiddle": "y",
|
||||
"advancedOption": "Avanzado",
|
||||
"borgTimerOnly": "Los tareas Borg están programados mediante temporizadores Monitor.",
|
||||
"borgTimerOnly": "Las tareas Borg se programan con temporizadores del Monitor.",
|
||||
"dailyOption": "A diario",
|
||||
"everyDayAt": "Todos los días a las {time}",
|
||||
"everyDayAtMidnight": "Todos los días a medianoche",
|
||||
|
||||
@@ -4562,6 +4562,7 @@
|
||||
"Passwords do not match. Please try again.": "Las contraseñas no coinciden. Por favor inténtalo de nuevo.",
|
||||
"Paste an oci-compose in the terminal": "Pegar un oci-compose en la terminal",
|
||||
"Paste it here and press Ctrl+D on an empty line.": "Pégalo aquí y pulsa Ctrl+D en una línea vacía.",
|
||||
"Paste it here and press Ctrl+D.": "Pégalo aquí y pulsa Ctrl+D.",
|
||||
"Paste its Compose file in the terminal": "Pegar su archivo Compose en la terminal",
|
||||
"Paste its docker run command in the terminal": "Pegar su comando docker run en la terminal",
|
||||
"Paste the UUP Dump URL here": "Pegue aquí la URL de UUP Dump",
|
||||
|
||||
@@ -3,6 +3,7 @@ is translated into the same template the catalog applications use."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
@@ -455,12 +456,24 @@ def describe_oci_compose(template: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def _read_pasted(ui, title: str | None = None, limit: int = MAX_COMPOSE_BYTES, missing: str | None = None) -> str:
|
||||
"""The definition is pasted in the terminal: dialog cannot take it."""
|
||||
"""The definition is pasted in the terminal: dialog cannot take it. On a
|
||||
terminal one Ctrl+D ends it, also at the end of a line that has text."""
|
||||
console.show_logo()
|
||||
console.msg_title(title or translate('Compose file of the application'))
|
||||
console.msg_info2(translate('Paste it here and press Ctrl+D on an empty line.'))
|
||||
console.msg_info2(translate('Paste it here and press Ctrl+D.'))
|
||||
print()
|
||||
text = sys.stdin.read(limit + 1)
|
||||
if sys.stdin.isatty():
|
||||
data = b''
|
||||
while len(data) <= limit:
|
||||
chunk = os.read(sys.stdin.fileno(), 65536)
|
||||
data += chunk
|
||||
# The terminal hands over whole lines; a chunk without its newline
|
||||
# is the line Ctrl+D sent before the end of the line.
|
||||
if not chunk.endswith(b'\n'):
|
||||
break
|
||||
text = data.decode('utf-8', errors='replace')
|
||||
else:
|
||||
text = sys.stdin.read(limit + 1)
|
||||
if not text.strip():
|
||||
raise UserCancelled(missing or (translate('No docker run command was given') if title is not None
|
||||
else translate('No Compose file was given')))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""A pasted definition ends with one Ctrl+D, also when it is pressed at the
|
||||
end of the last line instead of on an empty one."""
|
||||
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci import custom
|
||||
|
||||
|
||||
def read_pasted(limit):
|
||||
with patch.object(custom.console, "show_logo"), patch.object(custom.console, "msg_title"), \
|
||||
patch.object(custom.console, "msg_info2"), patch("builtins.print"):
|
||||
return custom._read_pasted(None, limit=limit)
|
||||
|
||||
COMPOSE = "services:\n app:\n image: nginx:latest\n"
|
||||
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "openpty"), "needs a pseudo terminal")
|
||||
class PasteUntilCtrlDTests(unittest.TestCase):
|
||||
def read(self, typed: bytes, limit: int = 1024) -> str:
|
||||
controller, terminal = os.openpty()
|
||||
with os.fdopen(terminal, "r") as stdin, patch("sys.stdin", stdin):
|
||||
os.write(controller, typed)
|
||||
try:
|
||||
return read_pasted(limit)
|
||||
finally:
|
||||
os.close(controller)
|
||||
|
||||
def test_ctrl_d_on_an_empty_line(self):
|
||||
self.assertEqual(self.read(COMPOSE.encode() + b"\x04"), COMPOSE)
|
||||
|
||||
def test_ctrl_d_at_the_end_of_the_last_line(self):
|
||||
self.assertEqual(self.read(COMPOSE.rstrip("\n").encode() + b"\x04"), COMPOSE.rstrip("\n"))
|
||||
|
||||
def test_reading_stops_past_the_limit(self):
|
||||
text = self.read(b"a: 1\n" * 20 + b"\x04", limit=10)
|
||||
self.assertGreater(len(text), 10)
|
||||
self.assertLess(len(text), 100)
|
||||
|
||||
|
||||
class PastedFromAPipeTests(unittest.TestCase):
|
||||
def test_a_pipe_is_read_to_its_end(self):
|
||||
with patch("sys.stdin", io.StringIO(COMPOSE)):
|
||||
self.assertEqual(read_pasted(1024), COMPOSE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user