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 && (
|
{!j.attached && j.next_run && (
|
||||||
<span className="inline-flex items-center gap-1" title={t("backup.jobs.nextRunTitle")}>
|
<span className="inline-flex items-center gap-1" title={t("backup.jobs.nextRunTitle")}>
|
||||||
<Clock className="h-3.5 w-3.5 text-green-500" />
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(statusBadge || lastRunWhen) && (
|
{(statusBadge || lastRunWhen) && (
|
||||||
|
|||||||
@@ -4016,7 +4016,7 @@
|
|||||||
},
|
},
|
||||||
"backends": {
|
"backends": {
|
||||||
"borgDescription": "Repositorio Borg para backups del host deduplicados.",
|
"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",
|
"borgShortDescription": "Repositorio Borg deduplicado",
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
"localDescription": "Carpeta en este host o en una unidad USB montada.",
|
"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.",
|
"localUsbDescription": "Carpeta en este host o una unidad USB montada.",
|
||||||
"managedByPve": "Gestionado por PVE",
|
"managedByPve": "Gestionado por PVE",
|
||||||
"manuallyAdded": "Agregado manualmente",
|
"manuallyAdded": "Agregado manualmente",
|
||||||
"nameCredentialsLocked": "El nombre y las credenciales 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 los tareas utilizan 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.",
|
"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.",
|
"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.",
|
"noPbsInWizard": "Aún no se han configurado repositorios de PBS.",
|
||||||
"not": "no",
|
"not": "no",
|
||||||
@@ -4141,7 +4141,7 @@
|
|||||||
"autoDetectedFromPveStorage": "Detectado desde el almacenamiento PVE",
|
"autoDetectedFromPveStorage": "Detectado desde el almacenamiento PVE",
|
||||||
"borgPassphraseLossBefore": "Si pierde la frase de contraseña de Borg, los backups cifrados no se podrán restaurar.",
|
"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.",
|
"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",
|
"borgPassphraseSavedAt": "La frase de contraseña se guarda en",
|
||||||
"encryptBackups": "Cifrar backups",
|
"encryptBackups": "Cifrar backups",
|
||||||
"encryptThisBackup": "Cifrar este backup",
|
"encryptThisBackup": "Cifrar este backup",
|
||||||
@@ -4251,7 +4251,7 @@
|
|||||||
"pathLabel": "Ruta",
|
"pathLabel": "Ruta",
|
||||||
"pbsRepository": "repositorio de PBS",
|
"pbsRepository": "repositorio de PBS",
|
||||||
"profileLabel": "Perfil",
|
"profileLabel": "Perfil",
|
||||||
"pveJobLabel": "tarea PvE",
|
"pveJobLabel": "Tarea PVE",
|
||||||
"pveStorage": "almacenamiento PVE",
|
"pveStorage": "almacenamiento PVE",
|
||||||
"pveVersion": "versión PvE",
|
"pveVersion": "versión PvE",
|
||||||
"recoveryPassphrase": "Frase de contraseña de recuperación",
|
"recoveryPassphrase": "Frase de contraseña de recuperación",
|
||||||
@@ -4288,20 +4288,20 @@
|
|||||||
"sshPort": "Puerto SSH"
|
"sshPort": "Puerto SSH"
|
||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"attachDescriptionAfter": "backups.",
|
"attachDescriptionAfter": "seleccionados.",
|
||||||
"attachDescriptionBefore": "Une Monitor a una tarea PVE vzdump y ejecútela después del",
|
"attachDescriptionBefore": "Vincula el Monitor a una tarea vzdump de PVE y se ejecuta tras el evento",
|
||||||
"attachDescriptionMiddle": "evento para seleccionados",
|
"attachDescriptionMiddle": "para los backups de",
|
||||||
"attachToPveJob": "Unirse a una tarea de Backup en PVE",
|
"attachToPveJob": "Unirse a una tarea de Backup en PVE",
|
||||||
"attachedToPveVzdump": "Adjunto a PVE vzdump",
|
"attachedToPveVzdump": "Adjunto a PVE vzdump",
|
||||||
"backendEditHelp": "El backend no se puede cambiar una vez creado la tarea.",
|
"backendEditHelp": "El backend no se puede cambiar una vez creada la tarea.",
|
||||||
"createJob": "crear tarea",
|
"createJob": "Crear tarea",
|
||||||
"createScheduledJob": "Crear tarea programado",
|
"createScheduledJob": "Crear tarea programada",
|
||||||
"customProfileTitle": "Perfil personalizado",
|
"customProfileTitle": "Perfil personalizado",
|
||||||
"defaultProfileTitle": "Perfil predeterminado",
|
"defaultProfileTitle": "Perfil predeterminado",
|
||||||
"detailDescription": "Programación, perfil, destino y última ejecución de esta tarea.",
|
"detailDescription": "Programación, perfil, destino y última ejecución de esta tarea.",
|
||||||
"disableJob": "Desactivar 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.",
|
"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",
|
"encryptedTitle": "cifrado",
|
||||||
"inheritedRetentionLabel": "Retención de la tarea PVE",
|
"inheritedRetentionLabel": "Retención de la tarea PVE",
|
||||||
"inheritedScheduleLabel": "Programación de la tarea PVE",
|
"inheritedScheduleLabel": "Programación de la tarea PVE",
|
||||||
@@ -4338,7 +4338,7 @@
|
|||||||
"runNowTitle": "Activa esta tarea ahora",
|
"runNowTitle": "Activa esta tarea ahora",
|
||||||
"running": "Ejecutando...",
|
"running": "Ejecutando...",
|
||||||
"scheduledTitle": "Programado",
|
"scheduledTitle": "Programado",
|
||||||
"standaloneScheduledJob": "Tarea programado independiente",
|
"standaloneScheduledJob": "Tarea programada independiente",
|
||||||
"starting": "Iniciando...",
|
"starting": "Iniciando...",
|
||||||
"stepOf": "Paso {step} de {total}",
|
"stepOf": "Paso {step} de {total}",
|
||||||
"summary": "Resumen",
|
"summary": "Resumen",
|
||||||
@@ -4592,7 +4592,7 @@
|
|||||||
"advancedHelpBefore": "Usar",
|
"advancedHelpBefore": "Usar",
|
||||||
"advancedHelpMiddle": "y",
|
"advancedHelpMiddle": "y",
|
||||||
"advancedOption": "Avanzado",
|
"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",
|
"dailyOption": "A diario",
|
||||||
"everyDayAt": "Todos los días a las {time}",
|
"everyDayAt": "Todos los días a las {time}",
|
||||||
"everyDayAtMidnight": "Todos los días a medianoche",
|
"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.",
|
"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 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 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 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 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",
|
"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
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
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:
|
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.show_logo()
|
||||||
console.msg_title(title or translate('Compose file of the application'))
|
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()
|
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():
|
if not text.strip():
|
||||||
raise UserCancelled(missing or (translate('No docker run command was given') if title is not None
|
raise UserCancelled(missing or (translate('No docker run command was given') if title is not None
|
||||||
else translate('No Compose file was given')))
|
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