diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index 8cad830..3a987cc 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -51,3 +51,26 @@ does not have, please pull the latest changes to avoid merge conflicts: git push origin 7. Finally, open a pull request to the original repository. + +Documentation and style +----------------------- + +Public Python interfaces need a docstring that follows ``docs/DOCSTRING_GUIDE.md`` +(Google style: summary line, ``Args``, ``Returns``, ``Raises``); C comments follow +the Doxygen rules in that same file. The guide is the single source of truth for +structure and wording, and ``docs/MAP.md`` maps where each kind of document +belongs. + +Run the checks that CI runs before you push: + +.. code-block:: bash + + uvx ruff check # style, including the docstring rules configured in pyproject.toml + uvx interrogate # public-API docstring coverage ratchet + +Docstrings carry the API detail (arguments, return values, exceptions); files +under ``docs/`` carry module purpose, tutorials and architecture only. Design +decisions belong in ``docs/design/`` (``docs/templates/adr.md``), per-PR change +notes in ``docs/changes/`` (``docs/templates/change-note.md``), and +``.github/PULL_REQUEST_TEMPLATE.md`` asks about both when you open the pull +request. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..48172ec --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,44 @@ +# Pull Request + +## Summary + + + +## Change type + +- [ ] New feature (Added) +- [ ] Behaviour change (Changed) +- [ ] Feature deprecated (Deprecated) +- [ ] Feature removed (Removed) +- [ ] Bug fix (Fixed) +- [ ] Security fix (Security) +- [ ] Docs / refactor (no user-visible change) + +## Checklist + +- [ ] Local tests pass (`pytest`) +- [ ] Ruff passes (`ruff check`) +- [ ] Related docs updated (docstring / .rst / README) +- [ ] Changelog updated (if there is a user-visible change) +- [ ] No new linter suppressions (`# noqa` / `# type: ignore`) + +--- + +## Documentation impact + +- [ ] No user-visible change +- [ ] Updated docs/... +- [ ] Changelog only +- [ ] Needs a follow-up issue #___ + +## Design decisions + +- [ ] No significant design decision in this change +- [ ] Added a design note: docs/design/___ +- [ ] Implicit assumptions to record (explained below) + +--- + +### Design note summary (if any) + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 393c689..98864fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,22 @@ jobs: run: uvx ty check continue-on-error: true + Docstrings: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Install uv + uses: astral-sh/setup-uv@v10.0.1 + - name: Docstring coverage ratchet (fails below the recorded baseline) + run: uvx interrogate + - name: Docstring style report (`docs/DOCSTRING_GUIDE.md` rules) + # `ruff check --select D` would override the ignore list in pyproject.toml, so filter + # the statistics table instead: pyproject.toml stays the single source of truth. + run: uvx ruff check --statistics 2>/dev/null | grep -E '^[[:space:]]*[0-9]+[[:space:]]+D[0-9]+' || true + continue-on-error: true + Test: strategy: fail-fast: false diff --git a/PyFlow/flow_setup.py b/PyFlow/flow_setup.py index 19256dd..73f8b91 100644 --- a/PyFlow/flow_setup.py +++ b/PyFlow/flow_setup.py @@ -52,6 +52,9 @@ "is_enable_encrypto": True, "is_custom_keys": None, "max_mem_buff": 2048, + "is_asynic_clients_io": False, + "is_debug": False, + "is_print_log": True, } CLIENT_DEFAULTS = { @@ -69,6 +72,8 @@ "is_enable_encrypto": True, "is_custom_keys": None, "max_mem_buff": 2048, + "is_debug": False, + "is_print_log": True, } @@ -262,11 +267,12 @@ def launch_web_tool(kind): server: host, port, max_clients, port_add_step, port_range_num, max_file_transfer_thread_num, is_hand_alloc_port, is_input_command_in_console, max_custom_workers, is_extend_command, - is_enable_encrypto, is_custom_keys, max_mem_buff + is_enable_encrypto, is_custom_keys, max_mem_buff, + is_asynic_clients_io, is_debug, is_print_log client: host, client_host, port, client_port, timeout, port_add_step, max_thread_num, is_input_command_in_console, is_wait_server, max_custom_workers, is_extend_command, is_enable_encrypto, - is_custom_keys, max_mem_buff + is_custom_keys, max_mem_buff, is_debug, is_print_log Booleans accept true/false/1/0/y/n; integers are parsed with int(). Type "none" to reset a nullable field (host/client_port/timeout/is_custom_keys). Help, Fix_Config, Setup and Quit work at every prompt, including field diff --git a/PyFlow/network_api/connect_tcp.py b/PyFlow/network_api/connect_tcp.py index 49a5c2f..6acb8c9 100644 --- a/PyFlow/network_api/connect_tcp.py +++ b/PyFlow/network_api/connect_tcp.py @@ -1,3 +1,26 @@ +"""TCP transport for PyFlow: the server and client base classes and the wire parsers. + +``TCP_Server_Base`` accepts connections and dispatches inbound lines; +``TCP_Client_Base`` connects, sends and reads on the same conventions: + +- one message per line, terminated by a newline; a line that starts with ``/`` + is a command and goes to the command handlers, anything else is a plain + message reported to the registered message listeners; +- an RSA-encrypted channel is negotiated right after connect unless + ``is_enable_encrypto`` is False; +- file/folder transfer, message forwarding and port allocation are layered on + the same socket and share its command namespace. + +The forwarding extensions use the module-level parsers +`parse_forwarded_message`, `parse_forward_items_and_addrs`, +`parse_forward_originator` and `forward_skip_message`. + +Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and +``TCP_Client_APIs.rst``; argument, return and exception contracts live in the +docstrings below. +""" + +import asyncio import os import ast import sys @@ -5,6 +28,7 @@ import copy import shlex import socket +import selectors import secrets import traceback import threading @@ -35,6 +59,37 @@ def _is_closed_socket_error(exc): ) MAX_DECODE_FAILURES = 3 # circuit breaker: close after N consecutive decode failures (re-exchange storm / garbage injection) +SEND_WAIT_TIMEOUT = 30.0 # seconds a non-blocking client write waits for buffer room + + +def _wait_writable(client_socket, timeout): + """Wait until a non-blocking socket accepts more data. + + Args: + client_socket (socket.socket): Socket to wait on; must be non-blocking. + timeout (float): Maximum seconds to wait. Must be > 0. + + Raises: + TimeoutError: If the socket stays unwritable for ``timeout`` seconds. + OSError: If the socket is closed or cannot be polled. + """ + with selectors.DefaultSelector() as selector: + selector.register(client_socket, selectors.EVENT_WRITE) + if not selector.select(timeout): + raise TimeoutError(f"peer did not accept data within {timeout} seconds") + + +def _log_line(enabled, parts): + """Print one command/result line when ``is_print_log`` allows it.""" + if enabled: + print(*parts) + + +def _debug_line(enabled, is_debug, parts): + """Print one execution-process line; needs ``is_print_log`` and ``is_debug``.""" + if enabled and is_debug: + print(*parts) + def _parse_destination_path(command_part): """Extract the trailing destination directory from a received transfer @@ -75,12 +130,14 @@ def _parse_destination_path(command_part): def parse_forwarded_message(command): """Split a ``/send_msg_from `` relay envelope. - The forward extension's server relay wraps every forwarded message - with the sender's address so the receiving client can attribute it - to the sending instance (the web tool shows it in the sender's - conversation). Returns ``(sender_id, payload)`` where ``sender_id`` - is the sender's ``"ip:port"``, or ``None`` when the command is not a - well-formed envelope. + Args: + command (str): Received line, e.g. + ``/send_msg_from ('127.0.0.1', 3000) hello``. + + Returns: + tuple | None: ``(sender_id, payload)`` where ``sender_id`` is the + sender's ``"ip:port"``, or None when the line is not a well-formed + envelope. """ try: parts = shlex.split(command) @@ -98,12 +155,18 @@ def parse_forwarded_message(command): def parse_forward_items_and_addrs(tokens): - """Split forward-command tokens into (items, destination addresses). + """Split forward-command tokens into items and destination addresses. + + A token of the form ``('ip', port)`` is a destination, everything else is a + forwarded item (message text or a path). Used by the native message + forwarding (``/forward_send_msg``) and by the file/folder forward extension. - A token of the form ``('ip', port)`` is a destination; anything else is - a forwarded item (message text or a path). Shared by the native - message forwarding (``/forward_send_msg``) and the file/folder forward - extension. + Args: + tokens (list[str]): Tokens after the command name. + + Returns: + tuple: ``(items, addrs)`` in the order given; ``items`` holds texts and + paths, ``addrs`` holds ``(ip, port)`` tuples. """ items = [] addrs = [] @@ -124,18 +187,33 @@ def parse_forward_items_and_addrs(tokens): def forward_skip_message(target): - """Console notice for a forward destination that cannot be served.""" + """Build the console notice for a forward destination that cannot be served. + + Args: + target (tuple): Destination ``(ip, port)`` that is unreachable or is the + server itself. + + Returns: + str: One-line notice for the console. + """ return f"forward: destination {target} is unreachable or is the server, skipped" def parse_forward_originator(command, own_address=None): """Extract the originator's ``"ip:port"`` from a received transfer command. + The server's forward relay tags every pushed ``/file`` and ``/file_folder`` - command with the forwarding client's address tuple (the tuple token before - the trailing transfer id). Direct sends carry the receiver's own address - instead, which is filtered out when ``own_address`` is given. Returns the - originator's ``"ip:port"``, or ``None`` when the command carries no - originator (a direct send or a non-transfer command). + command with the forwarding client's address tuple; a direct send carries the + receiver's own address instead. + + Args: + command (str): Received transfer command. + own_address (str | None): This instance's own ``"ip:port"``; a command + carrying it is a direct send and yields None. + + Returns: + str | None: Originator ``"ip:port"``, or None when the command carries no + originator (direct send or non-transfer command). """ try: parts = shlex.split(command) @@ -158,6 +236,29 @@ def parse_forward_originator(command, own_address=None): class TCP_Server_Base: # TCP server class + """TCP server: accept clients, dispatch commands, relay messages and files. + + Each accepted connection is served by `handle_client`, either in its own + thread (the default) or by a coroutine on an asyncio event loop when + ``is_asynic_clients_io`` is True: a line starting with ``/`` goes to + `handle_command` (built-in commands plus the handlers registered with + `register_command`), any other line is a plain message delivered to the + listeners registered with `add_message_listener` and stored in + ``messages_dict``. + + Attributes: + host (str): Address the server socket binds to. + port (int): First port considered for binding and for allocation. + clients (dict): Accepted connections keyed by ``(ip, port)``; each value + holds ``socket``, ``address``, ``id`` and ``connected_time``. + running (bool): True while the accept loop runs. + is_asynic_clients_io (bool): Whether clients are served by coroutines on + an event loop instead of one thread per client. + is_enable_encrypto (bool): Whether the RSA channel is negotiated. + is_debug (bool): Whether execution-process lines are logged as well. + is_print_log (bool): Whether the instance logs anything at all. + """ + def __init__( self, host="127.0.0.1", @@ -173,7 +274,49 @@ def __init__( is_enable_encrypto=True, is_custom_keys=None, max_mem_buff=2048, + is_asynic_clients_io=False, + is_debug=False, + is_print_log=True, ): + """Create the server and, unless extended, start accepting clients. + + Args: + host (str): Address the server socket binds to. Defaults to + "127.0.0.1". + port (int): First port to bind; also the base of the allocation range. + max_clients (int): Maximum concurrent clients served in thread mode; + ignored when ``is_asynic_clients_io`` is True. Defaults to 10. + port_add_step (int): Step between candidate ports. Defaults to 1. + port_range_num (int): Number of ports per step. Defaults to 100. + max_file_transfer_thread_num (int): Concurrent file transfers allowed. + Defaults to 10. + is_hand_alloc_port (bool): Reserve a port range across processes, so + several instances on one host do not collide. Defaults to False. + is_input_command_in_console (bool): Start the console command thread. + Defaults to True. + max_custom_workers (int): Worker slots for `submit_task` and threaded + command handlers. Defaults to 10. + is_extend_command (bool): When True, do not call `start_TCP_Server`; + the caller starts the server when ready. Defaults to False. + is_enable_encrypto (bool): Negotiate the RSA-encrypted channel for + every connection. Defaults to True. + is_custom_keys (list | None): ``[pub_key_path, pvt_key_path]`` pair used + instead of the default key lookup; an invalid pair is ignored. + max_mem_buff (int): Buffering ceiling in MiB for the in-memory forward + pump; past it the uploader is told to pause. Defaults to 2048. + is_asynic_clients_io (bool): Serve clients with asyncio coroutines on + an event loop instead of one thread per client, so a single + server can hold thousands of concurrent connections. Defaults to + False. + is_debug (bool): Log execution-process lines in addition to command + and result lines. Defaults to False. + is_print_log (bool): Log at all; False silences every line this + instance would print. Defaults to True. + + Raises: + OSError: If the ``.Flow`` directories or ``decode_command_table.json`` + cannot be created or read. + """ self.max_mem_buff = max_mem_buff * 1024 * 1024 self._forward_fid = 0 self._forward_fid_lock = threading.Lock() @@ -256,6 +399,11 @@ def __init__( self.is_extend_command = is_extend_command self.is_enable_encrypto = is_enable_encrypto self.is_custom_keys = is_custom_keys + self.is_asynic_clients_io = is_asynic_clients_io + self.is_debug = is_debug + self.is_print_log = is_print_log + self._async_loop = None # event loop driving the client coroutines (asynic clients io only) + self._async_wakeup = None # future `stop` completes to release the async accept loop self._crypto_lock = threading.RLock() # serialises the crypto collections below (no-GIL safe); held only around short ops, never across I/O self._encrypted_sockets = set() self._encrypted_recv_buffers = {} @@ -274,7 +422,36 @@ def __init__( else: self.start_TCP_Server() + def _log(self, *parts): + """Print a command/result line, unless ``is_print_log`` is False. + + Args: + *parts (Any): Values forwarded to ``print``. + """ + _log_line(self.is_print_log, parts) + + def _debug(self, *parts): + """Print an execution-process line; needs ``is_print_log`` and ``is_debug``. + + Args: + *parts (Any): Values forwarded to ``print``. + """ + _debug_line(self.is_print_log, self.is_debug, parts) + + def _log_exc(self): + """Print the traceback of the exception being handled, in debug mode only.""" + if self.is_print_log and self.is_debug: + traceback.print_exc() + def alloc_port(self, port_add_step, port_range_num): + """Reserve this server's port range under the cross-process lock. + + No-op unless ``is_hand_alloc_port`` is True. + + Args: + port_add_step (int): Step between candidate ports. + port_range_num (int): Number of ports per step. + """ if self.is_hand_alloc_port == True: while self.is_server_port_temp_info_file_locked(): time.sleep(0.1) @@ -283,6 +460,10 @@ def alloc_port(self, port_add_step, port_range_num): self.server_port_temp_info_file_unlock() def free_port(self): + """Release this server's reserved port range. + + No-op unless ``is_hand_alloc_port`` is True. + """ if self.is_hand_alloc_port == True: while self.is_server_port_temp_info_file_locked(): time.sleep(0.1) @@ -291,26 +472,45 @@ def free_port(self): self.server_port_temp_info_file_unlock() def server_port_temp_info_file_lock(self): + """Create the lock file that reserves the server port range for this process.""" with open(self.server_port_lock_file, "w", encoding="utf-8") as f: f.write("locked") def is_server_port_temp_info_file_locked(self): + """Report whether the server port range is reserved by some process. + + Returns: + bool: True while the lock file exists. + """ if os.path.exists(self.server_port_lock_file): return True else: return False def server_port_temp_info_file_unlock(self): + """Remove the lock file that reserves the server port range.""" if os.path.exists(self.server_port_lock_file): os.remove(self.server_port_lock_file) def hand_alloc_port(self, port_add_step, port_range_num): + """Allocate the next free server port range and record it on disk. + + ``port`` is moved past the ranges already recorded by other servers, so the + instance ends up with a range of its own. + + Args: + port_add_step (int): Step between candidate ports. + port_range_num (int): Number of ports per step. + + Raises: + OSError: If the server port info file cannot be read or written. + """ self.port_temp_info_path = os.path.join(self.project_temp_info_dir, "server_port_info.log") client_port_temp_info_file_path = os.path.join( self.project_temp_info_dir, "clients_port_info.log" ) if os.path.exists(client_port_temp_info_file_path): - print( + self._log( "Warning: client port info file exists, means the client has already allocated a port, may cause port conflict!" ) self.port_add_step = port_add_step @@ -370,6 +570,7 @@ def hand_alloc_port(self, port_add_step, port_range_num): f.write(str(self.server_port_info)) def hand_free_port(self): + """Drop this server's entry from the on-disk port range record.""" self.port_temp_info_path = os.path.join(self.project_temp_info_dir, "server_port_info.log") if os.path.exists(self.port_temp_info_path): with open(self.port_temp_info_path, "r", encoding="utf-8") as f: @@ -384,6 +585,12 @@ def hand_free_port(self): f.write(str(self.server_port_info)) def palloc(self): + """Allocate a transfer port, waiting until one is free. + + Returns: + int: Allocated port, or 0 when allocation is disabled + (``is_hand_alloc_port`` False). + """ alloc_port = 0 while True: alloc_port = self.file_palloc() @@ -398,10 +605,21 @@ def palloc(self): pass def pfree(self, port): + """Release a port obtained from `palloc`. + + Args: + port (int): Port to release. + """ self.file_pfree(port) self.spy_pfree(port) def file_palloc(self): + """Allocate the next port above the base, or the first free one in range. + + Returns: + int: Allocated port; None when the upward range is exhausted; 0 when + allocation is disabled (``is_hand_alloc_port`` False). + """ if self.is_hand_alloc_port: with self.alloc_add_port_lock: if self.add_latest_port + self.port_add_step > self.max_port: @@ -420,16 +638,27 @@ def file_palloc(self): return 0 def file_pfree(self, port): + """Release a port obtained from `file_palloc` and step the cursor back. + + Args: + port (int): Port to release. Ignored when allocation is disabled. + """ if self.is_hand_alloc_port: with self.alloc_add_port_lock: if port in self.all_allocated_ports_list: self.all_allocated_ports_list.remove(port) - print("releasing file transfer port, current latest port:", port) + self._debug("releasing file transfer port, current latest port:", port) self.add_latest_port -= self.port_add_step else: pass def spy_palloc(self): + """Allocate the next port below the base, or the first free one in range. + + Returns: + int: Allocated port; None when the downward range is exhausted; 0 when + allocation is disabled (``is_hand_alloc_port`` False). + """ if self.is_hand_alloc_port: with self.alloc_minus_port_lock: if self.minus_latest_port - self.port_add_step < self.min_port: @@ -448,6 +677,11 @@ def spy_palloc(self): return 0 def spy_pfree(self, port): + """Release a port obtained from `spy_palloc` and step the cursor back. + + Args: + port (int): Port to release. Ignored when allocation is disabled. + """ if self.is_hand_alloc_port: with self.alloc_minus_port_lock: if port in self.all_allocated_ports_list: @@ -457,31 +691,56 @@ def spy_pfree(self, port): pass def register_command(self, command_name, handler, where_to_run, run_in_thread=False): + """Register a custom command handler. + + Args: + command_name (str): Command to intercept, e.g. "/my_command"; matched + case-insensitively against the first token. + handler (callable): ``handler(client_socket, client_address, command)`` + called with the raw line; a non-None return value is sent back to + the sender as the response. + where_to_run (str): "server" for commands arriving from clients, + "client" for commands typed on this instance's console. + run_in_thread (bool): Run the handler on the worker pool instead of the + reader thread. Defaults to False. + + Returns: + bool | None: False when ``where_to_run`` is neither "server" nor + "client"; the handler is then not registered. + """ registe_index = None if where_to_run == "server": registe_index = 0 elif where_to_run == "client": registe_index = 1 else: - print(f"Invalid where_to_run value: {where_to_run}, must be 'server' or 'client'") + self._log(f"Invalid where_to_run value: {where_to_run}, must be 'server' or 'client'") return False self._custom_handlers[registe_index][command_name] = handler self._custom_handler_threaded[registe_index][command_name] = run_in_thread def add_message_listener(self, listener): - """Register ``listener(client_id, message)`` for every inbound plain-text message. + """Register ``listener(client_id, message)`` for every inbound plain message. + + Plain messages are the lines received from clients that do not start with + ``/``; commands go through the registered command handlers instead. - Plain messages are the chat/data lines received from a client that do - not start with ``/``. ``client_id`` is the sender's ``"ip:port"``. - Commands are not reported here; they go through the registered - command handlers. + Args: + listener (callable): ``listener(client_id, message)`` where + ``client_id`` is the sender's ``"ip:port"``. It runs on the receive + thread, so it must not block, and exceptions raised inside it are + swallowed. """ with self._event_listeners_lock: if listener not in self._message_listeners: self._message_listeners.append(listener) def remove_message_listener(self, listener): - """Unregister a listener previously added by ``add_message_listener``.""" + """Unregister a listener previously added by `add_message_listener`. + + Args: + listener (callable): Listener to remove; an unknown one is ignored. + """ with self._event_listeners_lock: try: self._message_listeners.remove(listener) @@ -489,20 +748,29 @@ def remove_message_listener(self, listener): pass def add_file_listener(self, listener): - """Register ``listener(client_id, full_path, name, size, command)`` for each saved inbound file. + """Register ``listener(client_id, full_path, name, size, command)`` per saved file. - Fired after a file uploaded by a client (a direct send or a forwarded + Fired after a file uploaded by a client (a direct send, or a forwarded file/folder item staged on the server) has been fully written to - ``file_transfer_dir``. ``client_id`` is the uploader's ``"ip:port"`` - and ``command`` is the wire command that triggered the transfer, so a - listener can recognise protocol pushes such as ``/crypto_pub_key``. + ``file_transfer_dir``. + + Args: + listener (callable): ``listener(client_id, full_path, name, size, + command)``; ``client_id`` is the uploader's ``"ip:port"`` and + ``command`` the wire command that triggered the transfer, so a + listener can recognise protocol pushes such as ``/crypto_pub_key``. + It runs on the transfer thread, so it must not block. """ with self._event_listeners_lock: if listener not in self._file_listeners: self._file_listeners.append(listener) def remove_file_listener(self, listener): - """Unregister a listener previously added by ``add_file_listener``.""" + """Unregister a listener previously added by `add_file_listener`. + + Args: + listener (callable): Listener to remove; an unknown one is ignored. + """ with self._event_listeners_lock: try: self._file_listeners.remove(listener) @@ -515,8 +783,9 @@ def _notify_message_received(self, client_id, message): for listener in listeners: try: listener(client_id, message) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"message listener error: {e}") + self._log_exc() def _notify_file_received(self, client_id, full_path, name, size, command): with self._event_listeners_lock: @@ -524,8 +793,9 @@ def _notify_file_received(self, client_id, full_path, name, size, command): for listener in listeners: try: listener(client_id, full_path, name, size, command) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"file listener error: {e}") + self._log_exc() def _socket_key(self, sock): """Serializable key for a sender socket (its peer address). @@ -690,16 +960,43 @@ def _merge_json_log(self, path, snapshot): except PermissionError: time.sleep(0.05) os.replace(tmp_path, path) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"log file update failed: {e}") + self._log_exc() def submit_task(self, func, *args, **kwargs): + """Run a callable on the instance's worker pool. + + Args: + func (callable): Callable to run. + *args (Any): Positional arguments forwarded to ``func``. + **kwargs (Any): Keyword arguments forwarded to ``func``. + + Returns: + concurrent.futures.Future: Handle for the submitted call; its worker + slot is released when the call finishes. + """ self._task_semaphore.acquire() future = self._custom_executor.submit(func, *args, **kwargs) future.add_done_callback(lambda f: self._task_semaphore.release()) return future def create_temporary_server(self, handler, port=None, max_connections=1): + """Start a temporary listener for a side channel (not the main protocol). + + Args: + handler (callable): ``handler(client_socket, address)`` started in its + own thread for every accepted connection. + port (int | None): Port to bind; None allocates one with `palloc`. + max_connections (int): Listen backlog. Defaults to 1. + + Returns: + tuple: ``(port, thread, stop_event)``; setting ``stop_event`` ends the + loop, which closes the socket and frees the port. + + Raises: + RuntimeError: If ``port`` is None and no port can be allocated. + """ if port is None: port = self.palloc() if port is None: @@ -720,7 +1017,7 @@ def server_loop(): continue except Exception as e: if not stop_event.is_set(): - print(f"Temporary server error: {e}") + self._log(f"Temporary server error: {e}") break server_socket.close() self.pfree(port) @@ -730,6 +1027,19 @@ def server_loop(): return port, server_thread, stop_event def create_temporary_client(self, server_host, server_port, bind_port=None, on_data=None): + """Open a temporary outbound connection for a side channel. + + Args: + server_host (str): Host to connect to. + server_port (int): Port to connect to. + bind_port (int | None): Local port to bind; None lets the OS choose. + on_data (callable | None): ``on_data(data, client_socket)`` called for + every received chunk. + + Returns: + tuple: ``(client_socket, thread, stop_event)``; setting ``stop_event`` + ends the receiver thread. + """ client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if bind_port is not None: client_sock.bind((self.host, bind_port)) @@ -758,6 +1068,15 @@ def receiver(): def broadcast( self, message, exclude_client=None ): # broadcast message to all clients except exclude_client + """Send one message to every connected client. + + Clients whose send fails are disconnected and removed from ``clients``. + + Args: + message (str | bytes): Payload passed to `send_message`. + exclude_client (tuple | None): ``(ip, port)`` to leave out, typically + the client the message came from. + """ with self.client_lock: disconnected_clients = [] for addr, client_info in self.clients.items(): @@ -767,16 +1086,24 @@ def broadcast( self.send_message(client_info["socket"], message) except: disconnected_clients.append(addr) - traceback.print_exc() + self._log_exc() for addr in disconnected_clients: # del disconnected clients if addr in self.clients: - print(f"deleting the disconnected client: {addr}") + self._debug(f"deleting the disconnected client: {addr}") self.clients[addr]["socket"].close() del self.clients[addr] def send_msg_to_specific_client( self, message ): # send message to specific client by client address + """Send the messages of a console line to the clients named in it. + + Args: + message (str): ``/send_msg`` line as typed: message text followed by one + or more ``(ip, port)`` identifiers; each message is delivered to the + identifiers that follow it. Addresses that are not connected are + skipped with a console notice. + """ command_part = shlex.split(message) del command_part[0] client_message_pair_list = [] @@ -794,8 +1121,8 @@ def send_msg_to_specific_client( client_message_pair = [client_list, msg_list] client_message_pair_list.append(client_message_pair) except: - traceback.print_exc() - print( + self._log_exc() + self._log( f"ErrorWhileParsingClientAddress: {part} is not a valid client address, skipped" ) else: @@ -815,7 +1142,7 @@ def send_msg_to_specific_client( client_socket = self.clients[client_addr]["socket"] self.send_message(client_socket, msg) else: - print(f"Client {client_addr} not found, cannot send message: {msg}") + self._log(f"Client {client_addr} not found, cannot send message: {msg}") def _crypto_get_send_lock(self, client_socket): """Per-socket lock serialising encrypt+send for one connection. @@ -832,8 +1159,23 @@ def _crypto_get_send_lock(self, client_socket): return lock def send_message(self, client_socket, message): # send message to specific client + """Write one line to a client socket, encrypting when the channel is up. + + Args: + client_socket (socket.socket): Target connection. + message (str | bytes): Payload; a str is stripped and newline + terminated, bytes are sent as they are. + + Returns: + bool: True when the payload was written, False for an unsupported + payload type. + + Raises: + RuntimeError: If the server is not running or no socket was passed. + OSError: If the socket write fails (the original error is re-raised). + """ if not self.running or not client_socket: - print("disable the connect to server") + self._log("disable the connect to server") raise RuntimeError("connection error") with self._crypto_get_send_lock(client_socket): # alloc+encrypt+sendall stay ordered per socket try: # add newline character for server to distinguish messages @@ -843,7 +1185,7 @@ def send_message(self, client_socket, message): # send message to specific clie if isinstance(message, bytes): message = message.decode("utf-8") if not isinstance(message, str): - print(f"Unsupported message type: {type(message)}") + self._log(f"Unsupported message type: {type(message)}") return False wire = self._crypto_encrypt_message(client_socket, message) data = wire.encode("ascii") + b"\n" @@ -855,22 +1197,49 @@ def send_message(self, client_socket, message): # send message to specific clie elif isinstance(message, bytes): data = message else: - print(f"Unsupported message type: {type(message)}") + self._log(f"Unsupported message type: {type(message)}") return False - client_socket.sendall(data) + self._sendall(client_socket, data) return True except Exception as e: if not _is_closed_socket_error(e): - print(f"send msg error: {e}") - traceback.print_exc() + self._log(f"send msg error: {e}") + self._log_exc() raise + def _sendall(self, client_socket, data): + """Write ``data`` in full, waiting for room on a non-blocking socket. + + Args: + client_socket (socket.socket): Target connection. + data (bytes): Payload to write in full. + + Raises: + OSError: If the socket write fails or the peer closed the + connection. + TimeoutError: If a non-blocking socket stays unwritable for + ``SEND_WAIT_TIMEOUT`` seconds. + """ + if not self.is_asynic_clients_io: # blocking socket: a single sendall is enough + client_socket.sendall(data) + return + view = memoryview(data) + while view: + try: + sent = client_socket.send(view) + except BlockingIOError: # non-blocking socket with a full send buffer + _wait_writable(client_socket, SEND_WAIT_TIMEOUT) + continue + if sent == 0: # the peer closed the connection + raise BrokenPipeError(errno.EPIPE, "socket closed while sending") + view = view[sent:] + def _send_raw(self, client_socket, text): """Send a plaintext crypto-protocol message, bypassing encryption.""" data = text.strip() if not data.endswith("\n"): data += "\n" - client_socket.sendall(data.encode("utf-8")) + self._sendall(client_socket, data.encode("utf-8")) def _crypto_mark_encrypted(self, client_socket, peer_role, peer_pem_path): with self._crypto_lock: @@ -912,6 +1281,15 @@ def _crypto_encrypt_message(self, client_socket, message): return f"{nonce}|{seq}|{body}" def receive_message(self, client_socket, msg_length): # receive message + """Read up to ``msg_length`` bytes from a client socket. + + Args: + client_socket (socket.socket): Connection to read from. + msg_length (int): Maximum number of bytes to read. + + Returns: + bytes: Received bytes, empty when the peer closed the connection. + """ data = client_socket.recv(msg_length) return data @@ -949,7 +1327,7 @@ def _crypto_process_line(self, client_socket, line): if state is None: return True, "" if nonce != state.get("peer_nonce"): - print("crypto: replay dropped (nonce mismatch)") + self._debug("crypto: replay dropped (nonce mismatch)") return True, "" try: seq = int(seq_str) @@ -957,7 +1335,7 @@ def _crypto_process_line(self, client_socket, line): return True, "" expected = state.get("recv_seq", 0) if seq != expected: - print( + self._debug( f"crypto: replay/out-of-order dropped (seq {seq}, expected {expected})" ) return True, "" @@ -1003,18 +1381,18 @@ def _crypto_on_decode_failure(self, client_socket): state["peer_pub_event"] = threading.Event() state["peer_pub_ok"] = None if do_close: - print("crypto: too many decode failures, closing connection") + self._log("crypto: too many decode failures, closing connection") try: client_socket.close() except Exception: - traceback.print_exc() + self._log_exc() return if do_reload and self.crypto is not None: try: self.crypto.reload_own_key() except Exception: - traceback.print_exc() - print("crypto: decode failure, re-exchanging public keys") + self._log_exc() + self._debug("crypto: decode failure, re-exchanging public keys") if do_exchange: if addr is not None: threading.Thread( @@ -1027,7 +1405,7 @@ def _crypto_on_decode_failure(self, client_socket): nonce = state.get("my_nonce") or self._crypto_fresh_nonce() self._send_raw(client_socket, f"/crypto_key_exchange {nonce} 1") except Exception: - traceback.print_exc() + self._log_exc() @staticmethod def _crypto_fresh_nonce(): @@ -1082,19 +1460,19 @@ def _crypto_wait_server_ready(self, client_socket, client_address, state): with self._crypto_lock: peer_pub_event = state.get("peer_pub_event") if peer_pub_event is None or not peer_pub_event.wait(timeout=60): - print("crypto: waiting for client public key timed out") + self._log("crypto: waiting for client public key timed out") return with self._crypto_lock: pub_ok = state.get("peer_pub_ok") if pub_ok is not True: - print("crypto: client public key rejected, connection dropped") + self._log("crypto: client public key rejected, connection dropped") return with self._crypto_lock: state["ready_sent"] = True try: self._send_raw(client_socket, "/crypto_ready") except Exception: - traceback.print_exc() + self._log_exc() self._crypto_try_server_flip(client_socket, state) def _crypto_push_pub_to_client(self, client_socket, client_address): @@ -1127,14 +1505,14 @@ def _crypto_push_pub_to_client(self, client_socket, client_address): break waiting_time += 1 if waiting_time >= 200: - print("crypto: transfer port waiting timeout, public key push failed") + self._log("crypto: transfer port waiting timeout, public key push failed") return False self.file_transfer_mode( self.crypto.pub_path, client_address[0], file_server_port, file_transfer_client_port ) self.pfree(file_transfer_client_port) except Exception: - traceback.print_exc() + self._log_exc() return False finally: with self._crypto_lock: @@ -1167,12 +1545,12 @@ def _crypto_store_received_pub(self, full_path, peer_role, client_address): ) state["peer_pub_ok"] = True state["peer_pub_event"].set() - print(f"crypto: accepted public key from {peer_ip}:{peer_port} ({reason})") + self._log(f"crypto: accepted public key from {peer_ip}:{peer_port} ({reason})") else: if state is not None: state["peer_pub_ok"] = False state["peer_pub_event"].set() - print(f"crypto: REJECTED public key from {peer_ip}:{peer_port}: {reason}") + self._log(f"crypto: REJECTED public key from {peer_ip}:{peer_port}: {reason}") try: os.remove(full_path) # rejected key: do not leave it in received_files/ except OSError: @@ -1182,13 +1560,144 @@ def _crypto_store_received_pub(self, full_path, peer_role, client_address): try: self._send_raw(peer_socket, f"/crypto_reject {reason}") except Exception: - traceback.print_exc() + self._log_exc() try: peer_socket.close() except Exception: - traceback.print_exc() + self._log_exc() def handle_client(self, client_socket, client_address): # deal with each client + """Serve one accepted client until it disconnects. + + Registers the client, greets it, announces the encryption mode and reads + lines until the peer closes: commands go to `handle_command`, plain + messages go to the message listeners and to ``messages_dict``. The client + is removed from ``clients`` and its socket closed when the read loop ends + for any reason. + + With ``is_asynic_clients_io`` False (the default) the call blocks and owns + its thread; with it True the client is served by a coroutine on the + server's event loop instead, and the call returns as soon as that + coroutine is scheduled. A connection arriving while that mode is on but + no event loop runs is closed with a console notice. + + Args: + client_socket (socket.socket): Accepted connection. + client_address (tuple): Peer ``(ip, port)``; used as the client id and + as the key in ``clients``. + """ + if self.is_asynic_clients_io: + self._schedule_client_coroutine(client_socket, client_address) + return + client_id = self._register_client(client_socket, client_address) + try: + if not self._announce_client(client_socket, client_id): + return + buffer = "" + while True: + data = self.receive_message(client_socket, 4096) # get msg from client + self._debug(data) + if not data: + break + buffer += data.decode("utf-8") + while "\n" in buffer: # deal with multiple messages in buffer + line, buffer = buffer.split("\n", 1) + message = line.strip() + if not message: + continue + response = self._process_client_line( + client_socket, client_address, client_id, message + ) + if response: # send response to client + self.send_message(client_socket, response) + except ConnectionResetError: + pass # peer dropped with RST; the finally block reports the disconnect once + except Exception as e: + if not _is_closed_socket_error(e): + self._log(f"error while deal with client {client_id} : {e}") + self._log_exc() + finally: + self._unregister_client(client_socket, client_address, client_id) + + def _schedule_client_coroutine(self, client_socket, client_address): + """Schedule one accepted client on the server's asyncio event loop. + + Args: + client_socket (socket.socket): Accepted connection. + client_address (tuple): Peer ``(ip, port)``. + """ + loop = self._async_loop + if loop is None or loop.is_closed(): + self._log( + "asynic clients io is enabled but no event loop is running, " + "closing the connection" + ) + try: + client_socket.close() + except Exception: + self._log_exc() + return + loop.call_soon_threadsafe( + loop.create_task, self._handle_client_async(client_socket, client_address) + ) + + async def _handle_client_async(self, client_socket, client_address): + """Serve one accepted client until it disconnects, as a coroutine. + + The socket is read through the event loop while the line dispatch and + every write run on worker threads, so a blocking command handler, + listener or peer cannot stall the other connections. + """ + loop = asyncio.get_running_loop() + client_id = self._register_client(client_socket, client_address) + try: + if not await asyncio.to_thread(self._announce_client, client_socket, client_id): + return + buffer = "" + while self.running: + data = await loop.sock_recv(client_socket, 4096) # get msg from client + self._debug(data) + if not data: + break + buffer += data.decode("utf-8") + while "\n" in buffer: # deal with multiple messages in buffer + line, buffer = buffer.split("\n", 1) + message = line.strip() + if not message: + continue + response = await asyncio.to_thread( + self._process_client_line, + client_socket, + client_address, + client_id, + message, + ) + if response: # send response to client + await asyncio.to_thread(self.send_message, client_socket, response) + except ConnectionResetError: + pass # peer dropped with RST; the finally block reports the disconnect once + except Exception as e: + if not _is_closed_socket_error(e): + self._log(f"error while deal with client {client_id} : {e}") + self._log_exc() + finally: + try: + # a cancelled read can leave its poll callback registered on the fd + loop.remove_reader(client_socket.fileno()) + except Exception: + pass + self._unregister_client(client_socket, client_address, client_id) + + def _register_client(self, client_socket, client_address): + """Add one accepted connection to ``clients``. + + Args: + client_socket (socket.socket): Accepted connection. + client_address (tuple): Peer ``(ip, port)``. + + Returns: + str: Client id, ``":"``. + """ client_id = f"{client_address[0]}:{client_address[1]}" with self.client_lock: # add new client self.clients[client_address] = { @@ -1200,90 +1709,131 @@ def handle_client(self, client_socket, client_address): # deal with each client if self.is_enable_encrypto and self.crypto is not None: with self._crypto_lock: self._crypto_sock_addr[client_socket] = client_address - print(f"new connection: {client_id}") - print(f"connection count mount: {len(self.clients)}") + self._debug(f"new connection: {client_id}") + self._debug(f"connection count mount: {len(self.clients)}") + return client_id + + def _announce_client(self, client_socket, client_id): + """Greet one client and announce the encryption mode and port range. + + Args: + client_socket (socket.socket): Connection to greet. + client_id (str): Client id used in the console log. + + Returns: + bool: True when the peer was greeted, False when it was already gone + (the caller closes the connection either way). + """ welcome_msg = f"Welcome!: {client_id}\n" # send welcome message try: self.send_message(client_socket, welcome_msg) except Exception as e: - # the server is stopping (or the peer vanished): the finally - # block below cleans up; never let this escape the thread + # the server is stopping (or the peer vanished): the caller's + # cleanup closes the connection; never let this escape if not _is_closed_socket_error(e): - print(f"error while welcoming client {client_id} : {e}") - return + self._log(f"error while welcoming client {client_id} : {e}") + return False # announce our encryption mode; a mismatched peer is disconnected in handle_command try: self._send_raw(client_socket, f"/crypto_mode {1 if self.is_enable_encrypto else 0}") except Exception as e: - # the peer vanished right after the welcome: the finally block - # below cleans up; never let this escape the thread + # the peer vanished right after the welcome: the caller's cleanup + # closes the connection; never let this escape if not _is_closed_socket_error(e): - print(f"error while announcing crypto mode to {client_id} : {e}") - return + self._log(f"error while announcing crypto mode to {client_id} : {e}") + return False if self.is_hand_alloc_port == True: - broadcast_clients_port_alloc_range_msg = "/client_alloc_port_range {}".format( + port_alloc_range_msg = "/client_alloc_port_range {}".format( self.each_client_port_range ) - self.broadcast(broadcast_clients_port_alloc_range_msg) else: - broadcast_clients_port_alloc_range_msg = "/client_alloc_port_range NO_LIMIT" - self.broadcast(broadcast_clients_port_alloc_range_msg) - print(self.clients) - buffer = "" + port_alloc_range_msg = "/client_alloc_port_range NO_LIMIT" + # the range is fixed for the server's lifetime, so only the connection + # that just joined needs it (announcing it to every client is O(N) per + # accepted connection) try: - while True: - data = self.receive_message(client_socket, 4096) # get msg from client - print(data) - if not data: - break - buffer += data.decode("utf-8") - while "\n" in buffer: # deal with multiple messages in buffer - line, buffer = buffer.split("\n", 1) - message = line.strip() - if not message: - continue - ok, plain = self._crypto_process_line(client_socket, message) - if ok: - message = plain.strip() - print(message) - if message.startswith("/"): # deal with special command - self._record_event(client_socket, message) - response = self.handle_command(client_socket, client_address, message) - else: - self._notify_message_received(client_id, message) - self._record_message(client_socket, message) - timestamp = datetime.now().strftime("%H:%M:%S") # deal with normal message - log_msg = f"[{timestamp}] {client_id}: {message}" - print(log_msg) - response = f"msg send: {message}" - if response: # send response to client - self.send_message(client_socket, response) - except ConnectionResetError: - pass # peer dropped with RST; the finally block reports the disconnect once + self.send_message(client_socket, port_alloc_range_msg) except Exception as e: + # the peer vanished right after the welcome: the caller's cleanup + # closes the connection; never let this escape if not _is_closed_socket_error(e): - print(f"error while deal with client {client_id} : {e}") - traceback.print_exc() - finally: - with self.client_lock: - if client_address in self.clients: - del self.clients[client_address] - with self._crypto_lock: - self._crypto_state.pop(client_address, None) - self._crypto_sock_addr.pop(client_socket, None) - self._crypto_peer.pop(client_socket, None) - self._crypto_push_active.discard(client_socket) - self._encrypted_sockets.discard(client_socket) - self._encrypted_recv_buffers.pop(client_socket, None) - self._crypto_send_locks.pop(client_socket, None) - client_socket.close() - print(f"client disconnected: {client_id}") - print(f"current connection count: {len(self.clients)}") + self._log(f"error while announcing the port range to {client_id} : {e}") + return False + self._debug(self.clients) + return True + + def _process_client_line(self, client_socket, client_address, client_id, message): + """Decrypt, log and dispatch one complete line from a client. + + Args: + client_socket (socket.socket): Connection the line came from. + client_address (tuple): Peer ``(ip, port)``. + client_id (str): Client id used in the console log. + message (str): One line with its newline removed. + + Returns: + str | None: Response for that client, or None when none is due. + """ + ok, plain = self._crypto_process_line(client_socket, message) + if ok: + message = plain.strip() + self._log(message) + if message.startswith("/"): # deal with special command + self._record_event(client_socket, message) + return self.handle_command(client_socket, client_address, message) + self._notify_message_received(client_id, message) + self._record_message(client_socket, message) + timestamp = datetime.now().strftime("%H:%M:%S") # deal with normal message + log_msg = f"[{timestamp}] {client_id}: {message}" + self._log(log_msg) + return f"msg send: {message}" + + def _unregister_client(self, client_socket, client_address, client_id): + """Drop one client's state and close its socket. + + Args: + client_socket (socket.socket): Connection to close. + client_address (tuple): Peer ``(ip, port)``. + client_id (str): Client id used in the console log. + """ + with self.client_lock: + if client_address in self.clients: + del self.clients[client_address] + with self._crypto_lock: + self._crypto_state.pop(client_address, None) + self._crypto_sock_addr.pop(client_socket, None) + self._crypto_peer.pop(client_socket, None) + self._crypto_push_active.discard(client_socket) + self._encrypted_sockets.discard(client_socket) + self._encrypted_recv_buffers.pop(client_socket, None) + self._crypto_send_locks.pop(client_socket, None) + client_socket.close() + self._debug(f"client disconnected: {client_id}") + self._debug(f"current connection count: {len(self.clients)}") def handle_command( self, client_socket, client_address, command ): # deal with special commands from client - print(client_socket, client_address, command) + """Dispatch one command line received from a client. + + Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, + ``/crypto_mode``, ``/file``, ``/file_folder``, + ``/server_file_transfer_port`` and the crypto exchange lines) are handled + here; any other name goes to the handlers registered for the "server" side + via `register_command`. An encryption-mode mismatch closes the connection; + an unknown command is only reported on the console. + + Args: + client_socket (socket.socket): Connection the line came from. + client_address (tuple): Peer ``(ip, port)``. + command (str): Line including its leading ``/``. + + Returns: + str | None: Response for that client, or None when no response is due + (crypto lines, file transfers, and custom handlers that run in the + background). + """ + self._debug(client_socket, client_address, command) client_id = f"{client_address[0]}:{client_address[1]}" send_str = None if command == "/help": @@ -1319,7 +1869,7 @@ def handle_command( except (IndexError, ValueError): client_crypto = -1 if client_crypto != (1 if self.is_enable_encrypto else 0): - print( + self._log( f"crypto: encryption mode mismatch with client {client_id} " f"(client={client_crypto}, server={1 if self.is_enable_encrypto else 0}), " f"disconnecting" @@ -1327,7 +1877,7 @@ def handle_command( try: client_socket.close() except Exception: - traceback.print_exc() + self._log_exc() return None elif shlex.split(command.lower())[0] == "/file": self.file_transfer_server_recv_server_start_thread(client_id, client_socket, command) @@ -1344,7 +1894,7 @@ def handle_command( [self.file_transfer_server_port, file_client_id] ) except: - traceback.print_exc() + self._log_exc() pass elif shlex.split(command.lower())[0] == "/forward_item": threading.Thread( @@ -1387,7 +1937,9 @@ def handle_command( with self._crypto_lock: # only accept pushes from a connection that started the handshake (else the TOFU registry could be poisoned) handshaking = client_address in self._crypto_state if not handshaking: - print(f"crypto: ignoring /crypto_pub_key from non-handshaking peer {client_id}") + self._log( + f"crypto: ignoring /crypto_pub_key from non-handshaking peer {client_id}" + ) return None self.file_transfer_server_recv_server_start_thread(client_id, client_socket, command) return None @@ -1442,11 +1994,11 @@ def handle_command( and command.lower().split(" ")[0] == "/crypto_reject" ): reason = command[len("/crypto_reject") :].strip() # the peer rejected our public key (TOFU mismatch) - print(f"crypto: connection rejected by peer: {reason}") + self._log(f"crypto: connection rejected by peer: {reason}") try: client_socket.close() except Exception: - traceback.print_exc() + self._log_exc() return None else: cmd_parts = shlex.split(command.strip()) @@ -1471,7 +2023,7 @@ def handle_command( ) return response else: - print(f"Unknown command: {command}") + self._log(f"Unknown command: {command}") def _handle_forward_send_msg(self, sock, addr, cmd): """Relay plain messages to every reachable destination client. @@ -1498,17 +2050,24 @@ def _handle_forward_send_msg(self, sock, addr, cmd): return None def forward_message_to(self, target, message, originator_addr): - """Send one plain message to ``target``, tagged with the originator's - address (public API for forward extensions). + """Send one plain message to a connected target, tagged with its origin. + + Public API for forward extensions: the message is wrapped in a + ``/send_msg_from `` envelope so the receiver can attribute + it to the originator (see `parse_forwarded_message`). - The message is wrapped in a ``/send_msg_from `` - envelope so the receiver can attribute it to the originator (see - ``parse_forwarded_message`` on the receiving side). Returns False when - the target is not connected. + Args: + target (tuple): Destination ``(ip, port)``. + message (str): Payload to deliver. + originator_addr (tuple): Originating client ``(ip, port)``. + + Returns: + bool: False when ``target`` is not connected (a console notice is + printed); True when the envelope was sent. """ client_info = self.clients.get(target) if client_info is None: - print(forward_skip_message(target)) + self._log(forward_skip_message(target)) return False self.send_message( client_info["socket"], @@ -1519,13 +2078,22 @@ def forward_message_to(self, target, message, originator_addr): def forward_target_command( self, kind, rel_dir, fname, originator_addr, tfid, destination_path=None ): - """Build the wire command that pushes one forwarded file/folder item to - a target, tagged with the originator's address (public API for forward - extensions). - - The originator tuple sits before the trailing transfer id: the - receiver's existing parsers treat it as the address slot and ignore - it, while ``parse_forward_originator`` recovers it for attribution. + """Build the tagged wire command that pushes one forwarded item. + + Public API for forward extensions. The originator tuple sits before the + trailing transfer id, where the receiver's existing parsers ignore it and + `parse_forward_originator` recovers it for attribution. + + Args: + kind (str): "file" or "file_folder". + rel_dir (str): Relative folder path (folders only). + fname (str): File or folder name. + originator_addr (tuple): Originating client ``(ip, port)``. + tfid (int): Transfer id shared by the pushed item. + destination_path (str | None): Receiver-side destination directory. + + Returns: + str: Command line to hand to `send_message`. """ originator = shlex.quote(repr(originator_addr)) if kind == "file": @@ -1545,16 +2113,28 @@ def forward_target_command( def forward_item_to( self, target, kind, rel_dir, fname, originator_addr, tfid, destination_path=None ): - """Push one forwarded file/folder item to ``target``, tagged with the - originator's address (public API for forward extensions). - - Sends the command built by ``forward_target_command``; the receiver - recovers the originator with ``parse_forward_originator``. Returns - False when the target is not connected. + """Push one forwarded file or folder item to a connected target. + + Public API for forward extensions: sends the line built by + `forward_target_command`, which the receiver attributes with + `parse_forward_originator`. + + Args: + target (tuple): Destination ``(ip, port)``. + kind (str): "file" or "file_folder". + rel_dir (str): Relative folder path (folders only). + fname (str): File or folder name. + originator_addr (tuple): Originating client ``(ip, port)``. + tfid (int): Transfer id shared by the pushed item. + destination_path (str | None): Receiver-side destination directory. + + Returns: + bool: False when ``target`` is not connected (a console notice is + printed); True when the command was sent. """ client_info = self.clients.get(target) if client_info is None: - print(forward_skip_message(target)) + self._log(forward_skip_message(target)) return False self.send_message( client_info["socket"], @@ -1573,16 +2153,16 @@ def _execute_custom_handler(self, handler, command, client_socket=None, client_a try: self.send_message(client_socket, result) except Exception as e: - print(f"Error sending message: {e}") + self._log(f"Error sending message: {e}") return result return None except Exception as e: error_msg = f"Error in custom command handler: {e}\n" - traceback.print_exc() + self._log_exc() try: self.send_message(client_socket, error_msg) except Exception as e: - print(f"Error sending error message: {e}") + self._log(f"Error sending error message: {e}") return error_msg def file_folder_transfer_server_recv_server_start_thread( # start a file folder server thread on server @@ -1681,7 +2261,7 @@ def file_transfer_client_recv(client_id): name_len_bytes = b"" while len(name_len_bytes) < 4: chunk = self.receive_message(client_file_socket, 4 - len(name_len_bytes)) - print(chunk) + self._debug(chunk) if not chunk: try: self.send_message(client_file_socket, self.error_sign) @@ -1756,7 +2336,7 @@ def file_transfer_client_recv(client_id): try: self.send_message(client_file_socket, self.error_sign) except: - traceback.print_exc() + self._log_exc() pass close_socket() raise ConnectionError( @@ -1773,7 +2353,7 @@ def file_transfer_client_recv(client_id): self._splice_event_command(command, fname=final_filename), datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) - print(f"file {filename} received from {client_id}, size {file_size} bytes") + self._log(f"file {filename} received from {client_id}, size {file_size} bytes") if command_part[0] == "/crypto_pub_key": with self._crypto_lock: peer_addr = self._crypto_sock_addr.get(client_socket) @@ -1789,10 +2369,10 @@ def file_transfer_client_recv(client_id): try: self.send_message(client_file_socket, self.server_received_file_data_sign) except Exception: - traceback.print_exc() + self._log_exc() close_socket() except Exception as e: - traceback.print_exc() + self._log_exc() if full_path is not None and os.path.exists(full_path): try: os.remove(full_path) # partial transfer: no half-written leftovers @@ -1803,7 +2383,7 @@ def file_transfer_client_recv(client_id): except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"ErrorWhileReceiveFile: {e}") + self._log(f"ErrorWhileReceiveFile: {e}") return False else: close_socket() @@ -1831,8 +2411,8 @@ def file_transfer_client_recv(client_id): target=file_transfer_client_recv, args=(client_id,), daemon=True ).start() except Exception as e: - print(f"\nget file transfer msg error: {e}") - traceback.print_exc() + self._log(f"\nget file transfer msg error: {e}") + self._log_exc() close_socket() finally: server_file_socket.close() @@ -1863,7 +2443,7 @@ def diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start(se file_client_pair = [client_list, file_list] file_client_pair_list.append(file_client_pair) except: - traceback.print_exc() + self._log_exc() else: if command_part_addr_times != 0: file_client_pair = [client_list, file_list] @@ -1892,8 +2472,8 @@ def diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start(se else: file_folder_transfer_command_message += " {}".format(shlex.quote(file)) except: - traceback.print_exc() - print( + self._log_exc() + self._log( f"ErrorWhileParsingFilePath: {file} is not a valid file or folder path, skipped" ) pass @@ -1940,7 +2520,7 @@ def multiple_file_multiple_client_transfer_server_recv_client_start(self, messag client_addr = ast.literal_eval(part) client_addr_list.append(client_addr) except: - traceback.print_exc() + self._log_exc() else: transfer_file_list.append(part) for client_addr in client_addr_list: @@ -1952,7 +2532,7 @@ def multiple_file_multiple_client_transfer_server_recv_client_start(self, messag elif os.path.isdir(transfer_file): item_type = "/file_folder" else: - print( + self._log( f"ErrorWhileParsingFilePath: {transfer_file} is not " "a valid file or folder path, skipped" ) @@ -1970,7 +2550,7 @@ def multiple_file_multiple_client_transfer_server_recv_client_start(self, messag self.file_transfer_server_recv_client_start_thread( file_transfer_command_message ) - print(f"start to send file command: {file_transfer_command_message}") + self._log(f"start to send file command: {file_transfer_command_message}") elif item_type == "/file_folder": folder_transfer_command_message = "/file_folder {} {}".format( shlex.quote(transfer_file), shlex.quote(str(client_addr)) @@ -1982,7 +2562,7 @@ def multiple_file_multiple_client_transfer_server_recv_client_start(self, messag self.folder_file_transfer_server_recv_client_start( folder_transfer_command_message ) - print(f"start to send folder command: {folder_transfer_command_message}") + self._log(f"start to send folder command: {folder_transfer_command_message}") def folder_file_transfer_server_recv_client_start(self, message): command_part = shlex.split(message) @@ -1995,7 +2575,7 @@ def folder_file_transfer_server_recv_client_start(self, message): client_addr = ast.literal_eval(command_part[-2]) client_socket = self.clients[client_addr]["socket"] if os.path.isdir(folder_path) == False: - print(f"{folder_path} is not a valid folder path") + self._log(f"{folder_path} is not a valid folder path") return False base_path = os.path.dirname(folder_path) @@ -2024,14 +2604,14 @@ def send_folder_transfer_command(folder_path, file_name=None, abspath=None): self.file_transfer_server_recv_client_start_thread( each_file_transfer_command_message, abspath ) - print(f"start to send folder command: {each_file_transfer_command_message}") + self._log(f"start to send folder command: {each_file_transfer_command_message}") else: if destination_path: folder_transfer_command_message += " {}".format( shlex.quote(destination_path) ) self.send_message(client_socket, folder_transfer_command_message.strip()) - print(f"start to send folder command: {folder_transfer_command_message}") + self._log(f"start to send folder command: {folder_transfer_command_message}") def start_file_transfer_with_limit(rel_dir, file, root): cmd = "/file_folder {} {} {}".format( @@ -2049,7 +2629,7 @@ def limited_transfer(): thread = threading.Thread(target=limited_transfer, daemon=True) thread.start() - print(f"start to send file: {cmd} (limit {self.max_file_transfer_thread_num})") + self._log(f"start to send file: {cmd} (limit {self.max_file_transfer_thread_num})") def get_all_files_in_folder(): for root, dirs, files in os.walk(folder_path): @@ -2058,7 +2638,7 @@ def get_all_files_in_folder(): send_folder_transfer_command(rel_dir) for file in files: start_file_transfer_with_limit(rel_dir, file, root) - print(f"finished sending all files in folder {folder_path}") + self._log(f"finished sending all files in folder {folder_path}") transfer_path = get_relative_path(base_path, folder_path) send_folder_transfer_command(transfer_path) @@ -2084,12 +2664,12 @@ def file_transfer_server_recv_client_start(self, message, file_folder_abspath): try: client_socket = self.clients[client_ip]["socket"] except: - print( + self._log( "ErrorWhileSearchingClientSocket: can not find the client socket, file sending failed" ) - traceback.print_exc() + self._log_exc() return False - print(client_socket, client_address, message) + self._debug(client_socket, client_address, message) with self.file_client_id_lock: client_id = copy.copy(self.file_client_id) send_msg = message.strip() + " " + str(self.file_client_id) + "\n" @@ -2117,7 +2697,7 @@ def file_transfer_server_recv_client_start(self, message, file_folder_abspath): pass waiting_time += 1 if waiting_time >= 20: - print( + self._log( "ErrorWhileReceiveFileServerPort: transfer port waiting timeout, file sending failed" ) return False @@ -2126,13 +2706,13 @@ def file_transfer_server_recv_client_start(self, message, file_folder_abspath): ) self.pfree(file_transfer_client_port) except IndexError: - print("invalid command, please use '/file '") - traceback.print_exc() + self._log("invalid command, please use '/file '") + self._log_exc() def file_transfer_mode( # noqa: PLR0911 - peer-close and timeout exits are distinct outcomes self, filename, server_address, server_port, client_port, pause_fid=None ): - print(f"start to send file: {filename}") + self._log(f"start to send file: {filename}") client_file_socket = None reset_time = 0 @@ -2151,11 +2731,11 @@ def close_socket(): client_file_socket.connect((server_address, server_port)) break except Exception as e: - print(f"file transfer connection error: {e}") - traceback.print_exc() + self._log(f"file transfer connection error: {e}") + self._log_exc() if reset_time >= 20: close_socket() - print("unable to connect to file transfer server, file sending failed") + self._log("unable to connect to file transfer server, file sending failed") return False reset_time += 1 time.sleep(1) @@ -2170,7 +2750,7 @@ def receive_file_transfer_messages(): try: data = self.receive_message(client_file_socket, 4096) if not data: - print("\nbreak the file transfer connection from server") + self._log("\nbreak the file transfer connection from server") try: self.send_message(client_file_socket, self.error_sign) except Exception: @@ -2179,13 +2759,15 @@ def receive_file_transfer_messages(): break file_receive_data_from_server = data.decode("utf-8").strip() if file_receive_data_from_server == self.error_sign: - print("\nError sign received from server, file transfer may have failed") + self._log( + "\nError sign received from server, file transfer may have failed" + ) close_socket() break except Exception as e: - print(f"\nget file transfer msg error: {e}") + self._log(f"\nget file transfer msg error: {e}") if not _is_closed_socket_error(e): - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: @@ -2214,7 +2796,7 @@ def receive_file_transfer_messages(): self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected - print( + self._log( f"ErrorWhileSendFile: \ Wait file transfer function start sign timeout, \ file {filename} sending failed" @@ -2258,33 +2840,33 @@ def receive_file_transfer_messages(): except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print( + self._log( f"ErrorWhileSendFileData: \ wait file transfer confirmation sign timeout, \ file {filename} sending may have failed" ) return False - print(f"Success: file {filename} sent successfully") + self._log(f"Success: file {filename} sent successfully") close_socket() return True except FileNotFoundError: - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"file {filename} not exist") + self._log(f"file {filename} not exist") return False except Exception as e: if not _is_closed_socket_error(e): - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"send error: {e}") + self._log(f"send error: {e}") return False # ---- native in-memory forward relay (server side) ---------------------- @@ -2344,10 +2926,10 @@ def _forward_item_handler(self, sock, addr, cmd): valid_targets = [] for target in addrs: if target == (self.host, self.port): - print(f"forward: target {target} is the server itself, skipped") + self._log(f"forward: target {target} is the server itself, skipped") continue if target not in self.clients: - print(f"forward: target {target} is unreachable, skipped") + self._log(f"forward: target {target} is unreachable, skipped") continue valid_targets.append(target) if not valid_targets: @@ -2477,11 +3059,11 @@ def _forward_relay( try: sock.connect((target[0], ports[tfid])) except Exception as e: - print(f"forward: cannot connect to target {target}: {e}") + self._log(f"forward: cannot connect to target {target}: {e}") sock.close() continue if not self._forward_read_sign(sock, self.server_start_file_transfer_sign): - print(f"forward: target {target} did not start, skipped") + self._log(f"forward: target {target} did not start, skipped") sock.close() continue target_conns.append((tfid, target, sock)) @@ -2576,7 +3158,7 @@ def reader(): size_b = self._forward_recv_exact(up_sock, 8) file_size = int.from_bytes(size_b, "big") except Exception as e: - print(f"forward: uploader header read failed: {e}") + self._log(f"forward: uploader header read failed: {e}") for q in queues.values(): q.put(END) return @@ -2606,18 +3188,32 @@ def reader(): self.send_message(up_sock, self.server_received_file_data_sign) except Exception: pass - print(f"forward: relayed {fname} to {result['ok']}/{len(target_conns)} targets") + self._log(f"forward: relayed {fname} to {result['ok']}/{len(target_conns)} targets") def start_TCP_Server(self): # set up server socket + """Bind the server socket, then accept clients until `stop` runs. + + Blocks the calling thread. A console command thread is started when + ``is_input_command_in_console`` is True. Every accepted connection is + served by `handle_client`: in its own thread by default, or by a + coroutine on an asyncio event loop when ``is_asynic_clients_io`` is True, + in which case ``max_clients`` no longer limits the connection count. + Socket errors and the end of the accept loop both end in `stop`. + """ try: self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.bind((self.host, self.port)) - self.server_socket.listen(self.max_clients) + self.server_socket.listen( + socket.SOMAXCONN if self.is_asynic_clients_io else self.max_clients + ) self.running = True - print(f"TCP server deployed on {self.host}:{self.port}") - print(f"max clients mount: {self.max_clients}") - print("input '/stop' to stop the server\n") + self._log(f"TCP server deployed on {self.host}:{self.port}") + if self.is_asynic_clients_io: + self._debug("clients io mode: asyncio, max clients mount: no limit") + else: + self._debug(f"max clients mount: {self.max_clients}") + self._log("input '/stop' to stop the server\n") if self.is_input_command_in_console: input_thread = threading.Thread( target=self.console_input, daemon=True @@ -2625,43 +3221,102 @@ def start_TCP_Server(self): # set up server socket input_thread.start() else: pass - while self.running: # main loop to accept clients + if self.is_asynic_clients_io: + asyncio.run(self._accept_clients_async()) + else: + self._accept_clients_threaded() + except Exception as e: + self._log(f"Server error: {e}") + self._log_exc() + finally: + self.stop() + + def _accept_clients_threaded(self): + """Accept clients until the server stops, one thread per client.""" + while self.running: # main loop to accept clients + try: + client_socket, client_address = self.server_socket.accept() + if len(self.clients) >= self.max_clients: + self.send_message(client_socket, "Max connection mount, try latter") + client_socket.close() + continue + client_thread = threading.Thread( # set up client handling thread + target=self.handle_client, args=(client_socket, client_address), daemon=True + ) + client_thread.start() + except OSError as e: + if not _is_closed_socket_error(e): + self._log(f"accept failed: {e}") + self._log_exc() + break # server socket closed, exit loop + + async def _accept_clients_async(self): + """Accept clients until the server stops, one coroutine per client. + + Runs the whole client side on one event loop: the listening socket is + used in non-blocking mode, and every accepted connection is handled by + `handle_client` on this loop, so the connection count is bounded only by + the file-descriptor limit. `stop` completes ``_async_wakeup`` to release + the loop; its remaining client coroutines are cancelled when it ends. + """ + loop = asyncio.get_running_loop() + self._async_loop = loop + wakeup = loop.create_future() + self._async_wakeup = wakeup + try: + self.server_socket.setblocking(False) + while self.running: + accept = loop.create_task(loop.sock_accept(self.server_socket)) + done, _ = await asyncio.wait({accept, wakeup}, return_when=asyncio.FIRST_COMPLETED) + if wakeup in done: + if accept.done() and not accept.cancelled() and accept.exception() is None: + accept.result()[0].close() # accepted while stopping: dropped + else: + accept.cancel() + await asyncio.gather(accept, return_exceptions=True) + break try: - client_socket, client_address = self.server_socket.accept() - if len(self.clients) >= self.max_clients: - self.send_message(client_socket, "Max connection mount, try latter") - client_socket.close() - continue - client_thread = threading.Thread( # set up client handling thread - target=self.handle_client, args=(client_socket, client_address), daemon=True - ) - client_thread.start() + client_socket, client_address = accept.result() except OSError as e: if not _is_closed_socket_error(e): - traceback.print_exc() + self._log(f"accept failed: {e}") + self._log_exc() break # server socket closed, exit loop - except Exception as e: - print(f"Server error: {e}") - traceback.print_exc() + except Exception as e: + self._log(f"accept loop error: {e}") + self._log_exc() + break + client_socket.setblocking(False) + self.handle_client(client_socket, client_address) finally: - self.stop() + self._async_loop = None + self._async_wakeup = None def console_input(self): # deal consule input + """Read console commands until the server stops. + + Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, + ``/file_folder``, ``/multiple_file_multiple_client``, + ``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward + commands are client-only and are refused here. Any other name goes to the + handlers registered with ``where_to_run="client"``. Ctrl-C and EOF stop the + server. + """ while self.running: try: cmd = input() deal_cmd = cmd.strip() if deal_cmd.lower() == "/stop": - print("shutting down...") + self._log("shutting down...") self.running = False self.stop() elif deal_cmd.lower() == "/status": - print(f"current connection count: {len(self.clients)}") - print(f"server running: {self.running}") + self._log(f"current connection count: {len(self.clients)}") + self._log(f"server running: {self.running}") elif deal_cmd.lower() == "/clients": with self.client_lock: for addr, info in self.clients.items(): - print(f" {info['id']} - connection time: {info['connected_time']}") + self._log(f" {info['id']} - connection time: {info['connected_time']}") elif shlex.split(deal_cmd)[0].lower() == "/send_msg": self.send_msg_to_specific_client(deal_cmd) elif shlex.split(deal_cmd)[0].lower() == "/file": @@ -2681,7 +3336,7 @@ def console_input(self): # deal consule input "/forward_file", "/forward_folder", ): - print( + self._log( "forward commands are client-only; " "run them on a client console, not on the server" ) @@ -2710,7 +3365,7 @@ def console_input(self): # deal consule input " with different file list for each client, files and clients", " should be in pairs, and clients should be in format of (ip, port)", ] - print("\n" + " ".join(help_text) + "\n") + self._log("\n" + " ".join(help_text) + "\n") else: cmd_parts = shlex.split(deal_cmd) if not cmd_parts: @@ -2726,40 +3381,86 @@ def console_input(self): # deal consule input response = self._execute_custom_handler(handler, deal_cmd) pass else: - print("Unrecognized command, input '/help' for available commands") + self._log("Unrecognized command, input '/help' for available commands") except KeyboardInterrupt: - print("\nKeyboardInterrupt received, shutting down...") + self._log("\nKeyboardInterrupt received, shutting down...") self.running = False self.stop() break except EOFError: - print("EOF received, shutting down...") + self._log("EOF received, shutting down...") self.running = False self.stop() break - except: - traceback.print_exc() + except Exception as e: + self._log(f"console command error: {e}") + self._log_exc() pass def stop(self): # shutting down the server + """Stop the server and release everything it owns. + + Closes the server socket and every client connection, flushes the message + and event stores, releases the allocated port range, clears ``running`` + and releases the client event loop when ``is_asynic_clients_io`` is True. + Safe to call more than once. + """ self.running = False self.free_port() self._flush_messages_dict() self._flush_events_dict() with self.client_lock: # close all clients connections for client_info in self.clients.values(): + try: + # a client thread blocked in recv() keeps the connection half + # open past close(): the shutdown sends its FIN and wakes it + client_info["socket"].shutdown(socket.SHUT_RDWR) + except OSError: + pass # already disconnected try: client_info["socket"].close() except: - traceback.print_exc() + self._log_exc() pass self.clients.clear() if self.server_socket: # close server socket self.server_socket.close() - print("server stopped") + self._log("server stopped") + self._wake_async_accept_loop() + + def _wake_async_accept_loop(self): + """Release the accept coroutine, if any, so its event loop can end.""" + loop = self._async_loop + wakeup = self._async_wakeup + if loop is None or loop.is_closed() or wakeup is None or wakeup.done(): + return + try: + loop.call_soon_threadsafe(wakeup.set_result, None) + except RuntimeError: + self._log_exc() class TCP_Client_Base: # TCP client class + """TCP client: connect to a server, dispatch commands, send and receive messages. + + Lines received from the server go through `receive_messages`: a line starting + with ``/`` is handled by `handle_server_command` (protocol commands plus the + handlers registered for the "server" side), any other line is a plain message + delivered to the listeners registered with `add_message_listener` and stored in + ``messages_dict``. With ``is_input_command_in_console`` the console thread + `interactive_mode` sends typed lines to the server. + + Attributes: + host (str): Server address this client connects to. + port (int): Server port this client connects to. + client_host (str): Local address the socket binds to. + client_port (int | None): Local port, None when the OS chose one. + running (bool): True while the connection is up. + is_enable_encrypto (bool): Whether the RSA channel is negotiated. + is_debug (bool): Whether execution-process lines are logged as well. + is_print_log (bool): Whether the instance logs anything at all. + """ + def __init__( self, host=None, @@ -2776,7 +3477,49 @@ def __init__( is_enable_encrypto=True, is_custom_keys=None, max_mem_buff=2048, + is_debug=False, + is_print_log=True, ): + """Create the client and, unless extended, connect and start reading. + + Args: + host (str | None): Server address to connect to; required before + `connect` is called. + client_host (str): Local address the socket binds to. Defaults to + "127.0.0.1". + port (int): Server port. Defaults to 65432. + client_port (int | None): Local port to bind; None lets the OS choose + an ephemeral port. + timeout (float | None): Socket timeout in seconds for connect and + receive. Must be None when ``is_wait_server`` is True. + port_add_step (int): Step between candidate ports in the allocation + range. Defaults to 1. + max_thread_num (int): Concurrent file transfers allowed. Defaults to 10. + is_input_command_in_console (bool): Enter interactive mode after + connecting. Defaults to True. + is_wait_server (bool): Keep retrying while the server is not reachable. + Defaults to True. + max_custom_workers (int): Worker slots for `submit_task` and threaded + command handlers. Defaults to 10. + is_extend_command (bool): When True, do not call `start_TCP_client`; the + caller connects when ready. Defaults to False. + is_enable_encrypto (bool): Negotiate the RSA-encrypted channel with the + server. Defaults to True. + is_custom_keys (list | None): ``[pub_key_path, pvt_key_path]`` pair used + instead of the default key lookup; an invalid pair is ignored. + max_mem_buff (int): Buffer ceiling in MiB, kept for parity with the + server class; the client's forward path does not read it today. + Defaults to 2048. + is_debug (bool): Log execution-process lines in addition to command + and result lines. Defaults to False. + is_print_log (bool): Log at all; False silences every line this + instance would print. Defaults to True. + + Raises: + ValueError: If ``is_wait_server`` is True and ``timeout`` is not None. + OSError: If the ``.Flow`` directories or ``decode_command_table.json`` + cannot be created or read. + """ self.max_mem_buff = max_mem_buff * 1024 * 1024 self._forward_upload_queue = queue.Queue() self._forward_pause = {} @@ -2870,6 +3613,8 @@ def __init__( self.is_extend_command = is_extend_command self.is_enable_encrypto = is_enable_encrypto self.is_custom_keys = is_custom_keys + self.is_debug = is_debug + self.is_print_log = is_print_log self._crypto_lock = threading.RLock() # serialises the crypto collections (no-GIL safe); held only around short ops, never across I/O self._encrypted_sockets = set() self._encrypted_recv_buffers = {} @@ -2907,33 +3652,57 @@ def __init__( self.start_TCP_client() def register_command(self, command_name, handler, where_to_run, run_in_thread=False): + """Register a custom command handler. + + Args: + command_name (str): Command to intercept, e.g. "/my_command"; matched + case-insensitively against the first token. + handler (callable): ``handler(client_socket, client_address, command)`` + called with the raw line; a non-None return value is sent back as + the response. + where_to_run (str): "server" for commands pushed by the server, "client" + for commands typed on this instance's console. + run_in_thread (bool): Run the handler on the worker pool instead of the + reader thread. Defaults to False. + + Returns: + bool | None: False when ``where_to_run`` is neither "server" nor + "client"; the handler is then not registered. + """ registe_index = None if where_to_run == "server": registe_index = 0 elif where_to_run == "client": registe_index = 1 else: - print(f"Invalid where_to_run value: {where_to_run}, must be 'server' or 'client'") + self._log(f"Invalid where_to_run value: {where_to_run}, must be 'server' or 'client'") return False self._custom_handlers[registe_index][command_name] = handler self._custom_handler_threaded[registe_index][command_name] = run_in_thread def add_message_listener(self, listener): - """Register ``listener(sender_id, message)`` for every inbound message. - - ``sender_id`` is the author's ``"ip:port"``: the forwarding client for - messages another client forwarded to this one (``/send_msg_from`` - envelopes), or ``None`` for direct pushes from the server, which do - not identify a client author. Commands are not reported here; they go - through the registered command handlers. Mirrors the server-side - contract (``listener(client_id, message)``). + """Register ``listener(sender_id, message)`` for every inbound plain message. + + Mirrors the server-side contract; commands are not reported here. + + Args: + listener (callable): ``listener(sender_id, message)``; ``sender_id`` is + the author's ``"ip:port"`` — the forwarding client for a message + another client forwarded here (``/send_msg_from`` envelope), or None + for a direct push from the server, which names no client author. It + runs on the receive thread, so it must not block, and exceptions + raised inside it are swallowed. """ with self._event_listeners_lock: if listener not in self._message_listeners: self._message_listeners.append(listener) def remove_message_listener(self, listener): - """Unregister a listener previously added by ``add_message_listener``.""" + """Unregister a listener previously added by `add_message_listener`. + + Args: + listener (callable): Listener to remove; an unknown one is ignored. + """ with self._event_listeners_lock: try: self._message_listeners.remove(listener) @@ -2941,19 +3710,27 @@ def remove_message_listener(self, listener): pass def add_file_listener(self, listener): - """Register ``listener(full_path, name, size, command)`` for each saved inbound file. + """Register ``listener(full_path, name, size, command)`` per saved inbound file. + + Fired after a file pushed by the server (a direct send, or a forwarded + file/folder item) has been fully written to ``file_transfer_dir``. - Fired after a file pushed by the server (a direct send, a forwarded - file or folder item) has been fully written to ``file_transfer_dir``. - ``command`` is the wire command that triggered the transfer, so a - listener can recognise protocol pushes such as ``/crypto_pub_key``. + Args: + listener (callable): ``listener(full_path, name, size, command)``; + ``command`` is the wire command that triggered the transfer, so a + listener can recognise protocol pushes such as ``/crypto_pub_key``. + It runs on the transfer thread, so it must not block. """ with self._event_listeners_lock: if listener not in self._file_listeners: self._file_listeners.append(listener) def remove_file_listener(self, listener): - """Unregister a listener previously added by ``add_file_listener``.""" + """Unregister a listener previously added by `add_file_listener`. + + Args: + listener (callable): Listener to remove; an unknown one is ignored. + """ with self._event_listeners_lock: try: self._file_listeners.remove(listener) @@ -2966,8 +3743,9 @@ def _notify_message_received(self, sender, message): for listener in listeners: try: listener(sender, message) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"message listener error: {e}") + self._log_exc() def _notify_file_received(self, full_path, name, size, command): with self._event_listeners_lock: @@ -2975,8 +3753,9 @@ def _notify_file_received(self, full_path, name, size, command): for listener in listeners: try: listener(full_path, name, size, command) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"file listener error: {e}") + self._log_exc() def _socket_key(self, sock): """Serializable key for a sender socket (its peer address). @@ -3141,16 +3920,57 @@ def _merge_json_log(self, path, snapshot): except PermissionError: time.sleep(0.05) os.replace(tmp_path, path) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"log file update failed: {e}") + self._log_exc() def submit_task(self, func, *args, **kwargs): + """Run a callable on the instance's worker pool. + + Args: + func (callable): Callable to run. + *args (Any): Positional arguments forwarded to ``func``. + **kwargs (Any): Keyword arguments forwarded to ``func``. + + Returns: + concurrent.futures.Future: Handle for the submitted call; its worker + slot is released when the call finishes. + """ self._task_semaphore.acquire() future = self._custom_executor.submit(func, *args, **kwargs) future.add_done_callback(lambda f: self._task_semaphore.release()) return future + def _log(self, *parts): + """Print a command/result line, unless ``is_print_log`` is False. + + Args: + *parts (Any): Values forwarded to ``print``. + """ + _log_line(self.is_print_log, parts) + + def _debug(self, *parts): + """Print an execution-process line; needs ``is_print_log`` and ``is_debug``. + + Args: + *parts (Any): Values forwarded to ``print``. + """ + _debug_line(self.is_print_log, self.is_debug, parts) + + def _log_exc(self): + """Print the traceback of the exception being handled, in debug mode only.""" + if self.is_print_log and self.is_debug: + traceback.print_exc() + def alloc_port(self, port_add_step, port_range_num): + """Reserve this client's port range under the cross-process lock. + + No-op until the server assigns a range (see ``/client_alloc_port_range``). + + Args: + port_add_step (int): Step between candidate ports. + port_range_num (int): Number of ports per step. + """ if self.is_hand_alloc_port == True: while self.is_client_port_temp_info_file_locked(): time.sleep(0.1) @@ -3159,6 +3979,10 @@ def alloc_port(self, port_add_step, port_range_num): self.client_port_temp_info_file_unlock() def free_port(self): + """Release this client's reserved port range. + + No-op unless a range was assigned (``is_hand_alloc_port`` True). + """ if self.is_hand_alloc_port == True: while self.is_client_port_temp_info_file_locked(): time.sleep(0.1) @@ -3167,26 +3991,45 @@ def free_port(self): self.client_port_temp_info_file_unlock() def client_port_temp_info_file_lock(self): + """Create the lock file that reserves the client port range for this process.""" with open(self.client_port_lock_file, "w", encoding="utf-8") as f: f.write("locked") def is_client_port_temp_info_file_locked(self): + """Report whether the client port range is reserved by some process. + + Returns: + bool: True while the lock file exists. + """ if os.path.exists(self.client_port_lock_file): return True else: return False def client_port_temp_info_file_unlock(self): + """Remove the lock file that reserves the client port range.""" if os.path.exists(self.client_port_lock_file): os.remove(self.client_port_lock_file) def hand_alloc_port(self, port_add_step, port_range_num): + """Allocate the next free client port range and record it on disk. + + ``port`` is moved past the ranges already recorded by other clients on this + host, so each instance ends up with a range of its own. + + Args: + port_add_step (int): Step between candidate ports. + port_range_num (int): Number of ports per step. + + Raises: + OSError: If the client port info file cannot be read or written. + """ self.port_temp_info_path = os.path.join(self.project_temp_info_dir, "clients_port_info.log") server_port_temp_info_file_path = os.path.join( self.project_temp_info_dir, "server_port_info.log" ) if os.path.exists(server_port_temp_info_file_path) == True: - print( + self._log( "Warning: server port info file exists, means the server has already allocated a port, may cause port conflict!" ) self.port_add_step = port_add_step @@ -3245,6 +4088,7 @@ def hand_alloc_port(self, port_add_step, port_range_num): f.write(str(self.client_port_info)) def hand_free_port(self): + """Drop this client's entry from the on-disk port range record.""" self.port_temp_info_path = os.path.join(self.project_temp_info_dir, "clients_port_info.log") if os.path.exists(self.port_temp_info_path): with open(self.port_temp_info_path, "r", encoding="utf-8") as f: @@ -3259,6 +4103,11 @@ def hand_free_port(self): f.write(str(self.client_port_info)) def palloc(self): + """Allocate a port, waiting until one is free. + + Returns: + int: Allocated port, or 0 when no allocation range was assigned. + """ alloc_port = 0 while True: alloc_port = self.file_palloc() @@ -3273,10 +4122,21 @@ def palloc(self): pass def pfree(self, port): + """Release a port obtained from `palloc`. + + Args: + port (int): Port to release. + """ self.file_pfree(port) self.spy_pfree(port) def file_palloc(self): + """Allocate the next port above the base, or the first free one in range. + + Returns: + int: Allocated port; None when the upward range is exhausted; 0 when no + allocation range was assigned. + """ if self.is_hand_alloc_port: with self.alloc_add_port_lock: if self.add_latest_port + self.port_add_step > self.max_port: @@ -3295,16 +4155,27 @@ def file_palloc(self): return 0 def file_pfree(self, port): + """Release a port obtained from `file_palloc` and step the cursor back. + + Args: + port (int): Port to release. Ignored when allocation is disabled. + """ if self.is_hand_alloc_port: with self.alloc_add_port_lock: if port in self.all_allocated_ports_list: self.all_allocated_ports_list.remove(port) - print("releasing file transfer port, current latest port:", port) + self._debug("releasing file transfer port, current latest port:", port) self.add_latest_port -= self.port_add_step else: pass def spy_palloc(self): + """Allocate the next port below the base, or the first free one in range. + + Returns: + int: Allocated port; None when the downward range is exhausted; 0 when + no allocation range was assigned. + """ if self.is_hand_alloc_port: with self.alloc_minus_port_lock: if self.minus_latest_port - self.port_add_step < self.min_port: @@ -3323,16 +4194,36 @@ def spy_palloc(self): return 0 def spy_pfree(self, port): + """Release a port obtained from `spy_palloc` and step the cursor back. + + Args: + port (int): Port to release. Ignored when allocation is disabled. + """ if self.is_hand_alloc_port: with self.alloc_minus_port_lock: if port in self.all_allocated_ports_list: self.all_allocated_ports_list.remove(port) - print("releasing file transfer port, current latest port:", port) + self._debug("releasing file transfer port, current latest port:", port) self.minus_latest_port += self.port_add_step else: pass def create_temporary_server(self, handler, port=None, max_connections=1): + """Start a temporary listener for a side channel (not the main protocol). + + Args: + handler (callable): ``handler(client_socket, address)`` started in its + own thread for every accepted connection. + port (int | None): Port to bind; None allocates one with `palloc`. + max_connections (int): Listen backlog. Defaults to 1. + + Returns: + tuple: ``(port, thread, stop_event)``; setting ``stop_event`` ends the + loop, which closes the socket and frees the port. + + Raises: + RuntimeError: If ``port`` is None and no port can be allocated. + """ if port is None: port = self.palloc() if port is None: @@ -3353,7 +4244,7 @@ def server_loop(): continue except Exception as e: if not stop_event.is_set(): - print(f"Temporary server error: {e}") + self._log(f"Temporary server error: {e}") break server_socket.close() self.pfree(port) @@ -3363,6 +4254,20 @@ def server_loop(): return port, server_thread, stop_event def create_temporary_client(self, server_host, server_port, bind_port=None, on_data=None): + """Open a temporary outbound connection for a side channel. + + Args: + server_host (str): Host to connect to. + server_port (int): Port to connect to. + bind_port (int | None): Local port to bind; None allocates one with + `palloc`. + on_data (callable | None): ``on_data(data, client_socket)`` called for + every received chunk. + + Returns: + tuple: ``(client_socket, thread, stop_event)``; setting ``stop_event`` + ends the receiver thread. + """ client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if bind_port is not None: client_sock.bind((self.client_host, bind_port)) @@ -3400,7 +4305,7 @@ def _crypto_negotiate_mode(self): """ self._send_raw(self.client_socket, f"/crypto_mode {1 if self.is_enable_encrypto else 0}") if not self._crypto_mode_event.wait(timeout=self._crypto_mode_timeout): - print("crypto: server did not announce its encryption mode, disconnecting") + self._log("crypto: server did not announce its encryption mode, disconnecting") self.close() return False if not self._crypto_mode_ok: @@ -3409,6 +4314,18 @@ def _crypto_negotiate_mode(self): return True def connect(self): # connect to server + """Connect to the server and start reading from it. + + Binds ``client_port`` when one was configured, then retries while + ``is_wait_server`` is True and the server is not reachable yet. Once the + socket is up the receive thread is started and the encryption mode is + negotiated, which closes the connection when the two sides disagree. + + Returns: + bool: True when the connection is established (and, if encryption is + enabled, the key exchange has been started); False when the attempt + failed or the mode negotiation closed the connection. + """ while True: try: self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -3417,7 +4334,7 @@ def connect(self): # connect to server self.client_socket.settimeout(self.timeout) # connect over 5 seconds timeout else: self.client_socket.settimeout(5) - print(f"connecting to {self.host}:{self.port}...") + self._log(f"connecting to {self.host}:{self.port}...") if self.client_port == None: pass else: @@ -3439,36 +4356,44 @@ def connect(self): # connect to server with self._crypto_lock: self._crypto_decode_failures = 0 # fresh connection, fresh breaker self._crypto_start_exchange() - print("connect success! type '/help' to get help.\n") + self._log("connect success! type '/help' to get help.\n") return True except socket.timeout: if self.is_wait_server: - print("waiting for server to start...") + self._log("waiting for server to start...") pass else: - print("timeout, unable to connect to server") - traceback.print_exc() + self._log("timeout, unable to connect to server") + self._log_exc() return False except ConnectionRefusedError: if self.is_wait_server: - print("waiting for server to start...") + self._log("waiting for server to start...") pass else: - print("connection rejected by server, please ensure the server is running") - traceback.print_exc() + self._log("connection rejected by server, please ensure the server is running") + self._log_exc() return False except Exception as e: - print(f"connection error: {e}") - traceback.print_exc() + self._log(f"connection error: {e}") + self._log_exc() return False def receive_messages(self): # get server msg + """Read from the server until the connection ends. + + Runs on the receive thread: plain lines are reported to the message + listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes are + attributed to their sender first), other ``/`` lines go to + `handle_server_command`. Any end of the connection clears ``running`` and + releases the port range. + """ buffer = "" while self.running: try: data = self.receive_message(self.client_socket, 4096) if not data: - print("\nbreak the connection from server") + self._log("\nbreak the connection from server") self.running = False self.free_port() break @@ -3501,26 +4426,38 @@ def receive_messages(self): # get server msg if not message.startswith("/"): self._notify_message_received(sender, message) self._record_message(sender or self.client_socket, message) - print(f"\n[server] {message}") + self._log(f"\n[server] {message}") except socket.timeout: continue except ConnectionResetError: - print("\nReset by server, connection closed") - traceback.print_exc() + self._log("\nReset by server, connection closed") + self._log_exc() self.running = False self.free_port() break except Exception as e: if not _is_closed_socket_error(e): - print(f"\nget msg error: {e}") - traceback.print_exc() + self._log(f"\nget msg error: {e}") + self._log_exc() self.running = False self.free_port() break def send_message(self, client_socket, message): # send msg to server + """Write one line to a socket, encrypting when the channel is up. + + Args: + client_socket (socket.socket): Target connection; the client passes + ``self.client_socket``. + message (str | bytes): Payload; a str is stripped and newline + terminated, bytes are sent as they are. + + Returns: + bool: True when the payload was written; False when the client is not + running, no socket was passed, or the payload type is unsupported. + """ if not self.running or not self.client_socket: - print("disable the connect to server") + self._log("disable the connect to server") return False with self._crypto_send_lock: # alloc+encrypt+sendall stay ordered (no-GIL safe) try: # add newline character for server to distinguish messages @@ -3530,7 +4467,7 @@ def send_message(self, client_socket, message): # send msg to server if isinstance(message, bytes): message = message.decode("utf-8") if not isinstance(message, str): - print(f"Unsupported message type: {type(message)}") + self._log(f"Unsupported message type: {type(message)}") return False wire = self._crypto_encrypt_message(client_socket, message) data = wire.encode("ascii") + b"\n" @@ -3542,20 +4479,38 @@ def send_message(self, client_socket, message): # send msg to server elif isinstance(message, bytes): data = message else: - print(f"Unsupported message type: {type(message)}") + self._log(f"Unsupported message type: {type(message)}") return False client_socket.sendall(data) return True except Exception as e: if not _is_closed_socket_error(e): - print(f"send msg error: {e}") - traceback.print_exc() + self._log(f"send msg error: {e}") + self._log_exc() return False def send_message_to_server(self, message): + """Send the payload of a console line to the server. + + Args: + message (str): Console line such as ``/send_msg hello``; the first token + (the command name) is dropped and the second one is sent. + + Raises: + IndexError: If the line has fewer than two tokens. + """ self.send_message(self.client_socket, shlex.split(message)[1]) def receive_message(self, client_socket, msg_length): # receive msg + """Read up to ``msg_length`` bytes from a socket. + + Args: + client_socket (socket.socket): Connection to read from. + msg_length (int): Maximum number of bytes to read. + + Returns: + bytes: Received bytes, empty when the peer closed the connection. + """ data = client_socket.recv(msg_length) return data @@ -3614,7 +4569,7 @@ def _crypto_process_line(self, client_socket, line): if ok: with self._crypto_lock: if nonce != self._crypto_server_nonce: - print("crypto: replay dropped (nonce mismatch)") + self._debug("crypto: replay dropped (nonce mismatch)") return True, "" try: seq = int(seq_str) @@ -3622,7 +4577,7 @@ def _crypto_process_line(self, client_socket, line): return True, "" expected = self._crypto_recv_seq if seq != expected: - print( + self._debug( f"crypto: replay/out-of-order dropped (seq {seq}, expected {expected})" ) return True, "" @@ -3655,25 +4610,25 @@ def _crypto_on_decode_failure(self, client_socket): do_exchange = True self._crypto_my_nonce = self._crypto_fresh_nonce() if do_close: - print("crypto: too many decode failures, closing connection") + self._log("crypto: too many decode failures, closing connection") try: self.close() except Exception: - traceback.print_exc() + self._log_exc() return if self.crypto is not None: try: self.crypto.reload_own_key() except Exception: - traceback.print_exc() - print("crypto: decode failure, re-exchanging public keys") + self._log_exc() + self._debug("crypto: decode failure, re-exchanging public keys") if do_exchange: try: threading.Thread( target=self._crypto_exchange_thread, args=(True,), daemon=True ).start() except Exception: - traceback.print_exc() + self._log_exc() def _crypto_start_exchange(self): """Start the client side of the public-key handshake (daemon).""" @@ -3727,8 +4682,8 @@ def _crypto_exchange_thread(self, force=False): if self._crypto_still_current(gen): raise TimeoutError("crypto handshake: server /crypto_ready never arrived") except Exception as e: - print(f"crypto handshake failed: {e}") - traceback.print_exc() + self._log(f"crypto handshake failed: {e}") + self._log_exc() try: self.close() except Exception: @@ -3798,7 +4753,7 @@ def _wait_done(): with self._crypto_lock: pub_event = self._crypto_server_pub_event if not pub_event.wait(timeout=90): - print("crypto: waiting for server public key timed out") + self._log("crypto: waiting for server public key timed out") return if push_thread is not None: # the push is best-effort (its ack can lag behind a slow file @@ -3853,14 +4808,14 @@ def _crypto_push_pub_key(self): break waiting_time += 1 if waiting_time >= 200: - print("crypto: transfer port waiting timeout, public key push failed") + self._log("crypto: transfer port waiting timeout, public key push failed") return False self.file_transfer_mode( self.crypto.pub_path, self.host, file_server_port, file_transfer_client_port ) self.pfree(file_transfer_client_port) except Exception: - traceback.print_exc() + self._log_exc() return False finally: with self._crypto_lock: @@ -3889,11 +4844,11 @@ def _crypto_store_received_pub(self, full_path, peer_role, server_addr): peer_role, peer_ip, peer_port ) self._crypto_server_pub_ok = True - print(f"crypto: accepted server public key ({reason})") + self._log(f"crypto: accepted server public key ({reason})") else: with self._crypto_lock: self._crypto_server_pub_ok = False - print(f"crypto: REJECTED server public key: {reason}") + self._log(f"crypto: REJECTED server public key: {reason}") try: os.remove(full_path) # rejected key: do not leave it in received_files/ except OSError: @@ -3901,16 +4856,28 @@ def _crypto_store_received_pub(self, full_path, peer_role, server_addr): try: self._send_raw(self.client_socket, f"/crypto_reject {reason}") except Exception: - traceback.print_exc() + self._log_exc() try: self.close() except Exception: - traceback.print_exc() + self._log_exc() return with self._crypto_lock: self._crypto_server_pub_event.set() def handle_server_command(self, command): # deal with special command from server + """Dispatch one command line pushed by the server. + + Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the + connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange lines, + and the transfer lines ``/file``, ``/file_folder``, ``/forward_upload``, + ``/pause_trans``, ``/start_trans``, ``/forward_error``. Any other name goes + to the handlers registered for the "server" side via `register_command`; an + unknown command is only reported on the console. + + Args: + command (str): Line including its leading ``/``. + """ client_id = f"{self.client_host}:{self.client_port}" if command.lower().split(" ")[0] == "/crypto_mode": try: @@ -3922,7 +4889,7 @@ def handle_server_command(self, command): # deal with special command from serv self._crypto_mode_ok = peer_mode == expected self._crypto_mode_event.set() if not self._crypto_mode_ok: - print( + self._log( f"crypto: encryption mode mismatch with server " f"(server={peer_mode}, client={expected}), disconnecting" ) @@ -3931,12 +4898,14 @@ def handle_server_command(self, command): # deal with special command from serv if command.lower().split(" ")[0] == "/client_alloc_port_range": if command.lower().split(" ")[1] == "no_limit": self.is_hand_alloc_port = False - print("server has no limit on client port allocation") + self._log("server has no limit on client port allocation") else: self.is_hand_alloc_port = True self.each_client_port_range = int(command.split(" ")[1]) self.alloc_port(self.port_add_step, self.each_client_port_range) - print(f"server allocated port range for each client: {self.each_client_port_range}") + self._log( + f"server allocated port range for each client: {self.each_client_port_range}" + ) elif shlex.split(command.lower())[0] == "/server_file_transfer_port": with self.file_transfer_server_port_lock: self.file_transfer_server_port = int(command.split(" ")[1]) @@ -3946,7 +4915,7 @@ def handle_server_command(self, command): # deal with special command from serv [self.file_transfer_server_port, file_client_id] ) except: - traceback.print_exc() + self._log_exc() pass elif ( self.is_enable_encrypto @@ -3976,7 +4945,7 @@ def handle_server_command(self, command): # deal with special command from serv and command.lower().split(" ")[0] == "/crypto_reject" ): reason = command[len("/crypto_reject") :].strip() - print(f"crypto: connection rejected by server: {reason}") + self._log(f"crypto: connection rejected by server: {reason}") self.close() elif ( self.is_enable_encrypto @@ -3986,7 +4955,7 @@ def handle_server_command(self, command): # deal with special command from serv with self._crypto_lock: handshake_started = self._crypto_handshake_started if not handshake_started: # only accept key pushes while a handshake is in progress - print("crypto: ignoring /crypto_pub_key outside a handshake") + self._debug("crypto: ignoring /crypto_pub_key outside a handshake") return self.file_transfer_client_recv_server_start_thread( # server pushes its key file; the receive hook TOFU-checks it client_id, self.client_socket, command @@ -4052,7 +5021,7 @@ def handle_server_command(self, command): # deal with special command from serv else: self._execute_custom_handler(handler, command, self.client_socket, client_id) else: - print(f"Unknown server command: {command}") + self._log(f"Unknown server command: {command}") def _console_forward_send_msg(self, command): """Client console entry point for ``/forward_send_msg`` (client-only). @@ -4064,7 +5033,7 @@ def _console_forward_send_msg(self, command): parts = shlex.split(command) items, addrs = parse_forward_items_and_addrs(parts[1:]) if not items or not addrs: - print( + self._log( "forward_send_msg: need at least one message and one destination, " 'e.g. /forward_send_msg "msg" "(\'127.0.0.1\', 3000)"' ) @@ -4074,11 +5043,17 @@ def _console_forward_send_msg(self, command): def forward_messages(self, messages, addrs): """Forward plain messages to other connected clients through the server. - Internal protocol feature (the client console command - ``/forward_send_msg``): this client must be connected. Each message is - sent to every destination over the server, wrapped there in a - ``/send_msg_from`` envelope so the receiver can attribute it back to - this client. ``addrs`` is a list of ``(ip, port)`` tuples. + The console command ``/forward_send_msg`` uses this; the client must be + connected. The server wraps each message in a ``/send_msg_from`` envelope so + the receiving client can attribute it back to this one. + + Args: + messages (list[str]): Message texts to forward. + addrs (list[tuple]): Destination ``(ip, port)`` tuples. + + Returns: + bool: True when the request was written to the server; False when the + client is not connected. """ request = "/forward_send_msg " + " ".join(shlex.quote(m) for m in messages) request += " " + " ".join(shlex.quote(str(a)) for a in addrs) @@ -4093,19 +5068,28 @@ def _execute_custom_handler(self, handler, command, client_socket=None, client_a try: self.send_message(client_socket, result) except Exception as e: - print(f"Error sending message: {e}") + self._log(f"Error sending message: {e}") return result return None except Exception as e: error_msg = f"Error in custom command handler: {e}\n" - traceback.print_exc() + self._log_exc() try: self.send_message(client_socket, error_msg) except Exception as e: - print(f"Error sending error message: {e}") + self._log(f"Error sending error message: {e}") return error_msg def interactive_mode(self): # Interactive mode + """Read console lines and act on them until the client stops. + + ``/quit`` closes the connection; ``/send_msg``, ``/file``, + ``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, + ``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are handled + locally; any other name goes to the handlers registered with + ``where_to_run="client"``, and anything left is sent to the server as it + stands. Ctrl-C and EOF close the connection. + """ client_id = f"{self.client_host}:{self.client_port}" try: while self.running: @@ -4156,23 +5140,23 @@ def interactive_mode(self): # Interactive mode ) else: self.send_message(self.client_socket, message) - print(f"Unknown server command: {message}") + self._log(f"Unknown server command: {message}") except KeyboardInterrupt: self.close() - print("\nshutting down...") - traceback.print_exc() + self._log("\nshutting down...") + self._log_exc() self.send_message(self.client_socket, "/quit") time.sleep(0.5) break except EOFError: self.close() - print("\nshutting down...") - traceback.print_exc() + self._log("\nshutting down...") + self._log_exc() self.send_message(self.client_socket, "/quit") time.sleep(0.5) break except: - traceback.print_exc() + self._log_exc() pass finally: self.close() @@ -4200,7 +5184,7 @@ def folder_file_transfer_client_recv_client_start(self, message): folder_path = command_part[1] destination_path = command_part[2] if len(command_part) >= 3 else None if os.path.isdir(folder_path) == False: - print(f"{folder_path} is not a valid folder path") + self._log(f"{folder_path} is not a valid folder path") return False base_path = os.path.dirname(folder_path) @@ -4229,14 +5213,14 @@ def send_folder_transfer_command(folder_path, file_name=None, abspath=None): self.file_transfer_client_recv_client_start_thread( each_file_transfer_command_message, abspath ) - print(f"start to send folder command: {each_file_transfer_command_message}") + self._log(f"start to send folder command: {each_file_transfer_command_message}") else: if destination_path: folder_transfer_command_message += " {}".format( shlex.quote(destination_path) ) self.send_message(self.client_socket, folder_transfer_command_message.strip()) - print(f"start to send folder command: {folder_transfer_command_message}") + self._log(f"start to send folder command: {folder_transfer_command_message}") def start_file_transfer_with_limit(rel_dir, file, root): cmd = f"/file_folder {shlex.quote(rel_dir)} {shlex.quote(file)}" @@ -4252,7 +5236,7 @@ def limited_transfer(): thread = threading.Thread(target=limited_transfer, daemon=True) thread.start() - print(f"start to send file: {cmd} (limit {self.max_thread_num})") + self._log(f"start to send file: {cmd} (limit {self.max_thread_num})") def get_all_files_in_folder(): for root, dirs, files in os.walk(folder_path): @@ -4261,7 +5245,7 @@ def get_all_files_in_folder(): send_folder_transfer_command(rel_dir) for file in files: start_file_transfer_with_limit(rel_dir, file, root) - print(f"finished sending all files in folder {folder_path}") + self._log(f"finished sending all files in folder {folder_path}") transfer_path = get_relative_path(base_path, folder_path) send_folder_transfer_command(transfer_path) @@ -4283,7 +5267,7 @@ def multiple_file_transfer_client_recv_client_start(self, message): self.file_transfer_client_recv_client_start_thread( each_file_transfer_command_message ) - print(f"start to send file command: {each_file_transfer_command_message}") + self._log(f"start to send file command: {each_file_transfer_command_message}") finally: self.file_semaphore.release() @@ -4324,7 +5308,7 @@ def file_transfer_client_recv_client_start(self, message, file_folder_abspath): pass waiting_time += 1 if waiting_time >= 20: - print( + self._log( "ErrorWhileReceiveFileServerPort: transfer port waiting timeout, file sending failed" ) return False @@ -4333,13 +5317,13 @@ def file_transfer_client_recv_client_start(self, message, file_folder_abspath): ) self.pfree(file_transfer_client_port) except IndexError: - traceback.print_exc() - print("invalid command, please use '/file '") + self._log_exc() + self._log("invalid command, please use '/file '") def file_transfer_mode( # noqa: PLR0911 - peer-close and timeout exits are distinct outcomes self, filename, server_address, server_port, client_port, pause_fid=None ): - print(f"start to send file: {filename}") + self._log(f"start to send file: {filename}") client_file_socket = None reset_time = 0 @@ -4358,11 +5342,11 @@ def close_socket(): client_file_socket.connect((server_address, server_port)) break except Exception as e: - print(f"file transfer connection error: {e}") - traceback.print_exc() + self._log(f"file transfer connection error: {e}") + self._log_exc() if reset_time >= 20: close_socket() - print("unable to connect to file transfer server, file sending failed") + self._log("unable to connect to file transfer server, file sending failed") return False reset_time += 1 time.sleep(1) @@ -4377,7 +5361,7 @@ def receive_file_transfer_messages(): try: data = self.receive_message(client_file_socket, 4096) if not data: - print("\nbreak the file transfer connection from server") + self._log("\nbreak the file transfer connection from server") try: self.send_message(client_file_socket, self.error_sign) except Exception: @@ -4386,13 +5370,15 @@ def receive_file_transfer_messages(): break file_receive_data_from_server = data.decode("utf-8").strip() if file_receive_data_from_server == self.error_sign: - print("\nError sign received from server, file transfer may have failed") + self._log( + "\nError sign received from server, file transfer may have failed" + ) close_socket() break except Exception as e: - print(f"\nget file transfer msg error: {e}") + self._log(f"\nget file transfer msg error: {e}") if not _is_closed_socket_error(e): - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: @@ -4421,7 +5407,7 @@ def receive_file_transfer_messages(): self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected - print( + self._log( f"ErrorWhileSendFile: \ Wait file transfer function start sign timeout, \ file {filename} sending failed" @@ -4465,33 +5451,33 @@ def receive_file_transfer_messages(): except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print( + self._log( f"ErrorWhileSendFileData: \ wait file transfer confirmation sign timeout, \ file {filename} sending may have failed" ) return False - print(f"Success: file {filename} sent successfully") + self._log(f"Success: file {filename} sent successfully") close_socket() return True except FileNotFoundError: - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"file {filename} not exist") + self._log(f"file {filename} not exist") return False except Exception as e: if not _is_closed_socket_error(e): - traceback.print_exc() + self._log_exc() try: self.send_message(client_file_socket, self.error_sign) except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"send error: {e}") + self._log(f"send error: {e}") return False # ---- native in-memory forward (client side) ---------------------------- @@ -4534,11 +5520,11 @@ def forward_file_console(self, message): files = [p for p in items if os.path.isfile(p)] for p in items: if not os.path.isfile(p): - print(f"forward: {p} is not a valid file, skipped") + self._log(f"forward: {p} is not a valid file, skipped") if not files: return if not addrs: - print("forward: no target clients given") + self._log("forward: no target clients given") return threading.Thread( target=self._forward_driver, @@ -4553,11 +5539,11 @@ def forward_folder_console(self, message): folders = [p for p in items if os.path.isdir(p)] for p in items: if not os.path.isdir(p): - print(f"forward: {p} is not a valid folder, skipped") + self._log(f"forward: {p} is not a valid folder, skipped") if not folders: return if not addrs: - print("forward: no target clients given") + self._log("forward: no target clients given") return threading.Thread( target=self._forward_driver, @@ -4586,8 +5572,9 @@ def _forward_driver(self, items, addrs, folder_mode, destination_path=None): addrs, destination_path, ) - except Exception: - traceback.print_exc() + except Exception as e: + self._log(f"forward error: {e}") + self._log_exc() def _forward_rel_path(self, base_path, abs_path): base = os.path.normpath(base_path) @@ -4602,7 +5589,7 @@ def _forward_rel_path(self, base_path, abs_path): def _forward_one(self, kind, rel_dir, fname, abspath, addrs, destination_path=None): if not os.path.isfile(abspath): - print(f"forward: {abspath} is not a valid file, skipped") + self._log(f"forward: {abspath} is not a valid file, skipped") return msg = f"/forward_item {kind}" if kind == "folder": @@ -4616,7 +5603,7 @@ def _forward_one(self, kind, rel_dir, fname, abspath, addrs, destination_path=No deadline = time.time() + 30 while time.time() < deadline: if self._forward_error is not None: - print(f"forward: server error: {self._forward_error}") + self._log(f"forward: server error: {self._forward_error}") self._forward_error = None return try: @@ -4625,7 +5612,7 @@ def _forward_one(self, kind, rel_dir, fname, abspath, addrs, destination_path=No except queue.Empty: continue if upload is None: - print(f"forward: timed out waiting for an upload slot for {fname}") + self._log(f"forward: timed out waiting for an upload slot for {fname}") return fid, sport = upload client_port = self.palloc() @@ -4730,7 +5717,7 @@ def file_transfer_client_recv(client_id): name_len_bytes = b"" while len(name_len_bytes) < 4: chunk = self.receive_message(client_file_socket, 4 - len(name_len_bytes)) - print(chunk) + self._debug(chunk) if not chunk: try: self.send_message(client_file_socket, self.error_sign) @@ -4805,7 +5792,7 @@ def file_transfer_client_recv(client_id): try: self.send_message(client_file_socket, self.error_sign) except: - traceback.print_exc() + self._log_exc() pass close_socket() raise ConnectionError( @@ -4822,16 +5809,16 @@ def file_transfer_client_recv(client_id): self._splice_event_command(command, fname=final_filename), datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) - print(f"file {filename} received from {client_id}, size {file_size} bytes") + self._log(f"file {filename} received from {client_id}, size {file_size} bytes") if command_part[0] == "/crypto_pub_key": self._crypto_store_received_pub(full_path, "server", (self.host, self.port)) try: self.send_message(client_file_socket, self.server_received_file_data_sign) except Exception: - traceback.print_exc() + self._log_exc() close_socket() except Exception as e: - traceback.print_exc() + self._log_exc() if full_path is not None and os.path.exists(full_path): try: os.remove(full_path) # partial transfer: no half-written leftovers @@ -4842,7 +5829,7 @@ def file_transfer_client_recv(client_id): except Exception: pass # send_message already logged real errors; a dead peer is expected close_socket() - print(f"ErrorWhileReceiveFile: {e}") + self._log(f"ErrorWhileReceiveFile: {e}") return False else: close_socket() @@ -4870,22 +5857,41 @@ def file_transfer_client_recv(client_id): target=file_transfer_client_recv, args=(client_id,), daemon=True ).start() except Exception as e: - print(f"\nget file transfer msg error: {e}") - traceback.print_exc() + self._log(f"\nget file transfer msg error: {e}") + self._log_exc() close_socket() finally: server_file_socket.close() def close(self): # close connection + """Close the connection and release everything the client owns. + + Stops the receive loop, releases the port range, flushes the message and + event stores and closes the socket. Safe to call more than once. + """ self.running = False self.free_port() self._flush_messages_dict() self._flush_events_dict() if self.client_socket: + try: + # the receive thread may be blocked in recv() on this socket: + # close() alone sends no FIN and leaves that read waiting for its + # timeout, so the server would not see the disconnect + self.client_socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass # peer already gone self.client_socket.close() - print("connection closed") + self._log("connection closed") def start_TCP_client(self): # start client + """Connect to the server and start the client loop. + + Enters `interactive_mode` when ``is_input_command_in_console`` is True, + otherwise keeps the process alive while the connection is up. Exits the + process with status 1 when the connection cannot be established; Ctrl-C and + the end of the connection both run `close`. + """ if not self.connect(): sys.exit(1) try: @@ -4895,7 +5901,7 @@ def start_TCP_client(self): # start client while self.running: time.sleep(1) except KeyboardInterrupt: - print("\nclient shutting down...") - traceback.print_exc() + self._log("\nclient shutting down...") + self._log_exc() finally: self.close() diff --git a/PyFlow/transfer_web/setup_client.py b/PyFlow/transfer_web/setup_client.py index b75ad10..b0862e2 100755 --- a/PyFlow/transfer_web/setup_client.py +++ b/PyFlow/transfer_web/setup_client.py @@ -5,7 +5,10 @@ UI in the browser. The user enters the server address (an http/https domain or a bare IP); the backend asks the server's web backend for the TCP server address/port, starts the TCP client, and keeps the backend -running to relay the user's frontend actions. +running to relay the user's frontend actions. The account of the server +logs in from the same backend: saved credentials in +``.Flow_Web/client_login.json`` are replayed on every start, and the +login window appears whenever no session could be restored. """ import os @@ -20,6 +23,7 @@ def main(): + """Start the client web backend and serve its UI.""" app = ClientWebApp() app.start_from_config() app.run() diff --git a/PyFlow/transfer_web/setup_server.py b/PyFlow/transfer_web/setup_server.py index 68e5b4a..600f74a 100755 --- a/PyFlow/transfer_web/setup_server.py +++ b/PyFlow/transfer_web/setup_server.py @@ -10,7 +10,10 @@ After the TCP server is up, the lightweight Flask backend serves the status page and the client-facing API (``/api/server_info`` etc.) on -the server's address. +the server's address. Anonymous visitors also get the landing page +with the registration and password-reset flows; accounts live in the +SQLite store ``.Flow_Web/flow_web.db``, and the verification mails are +sent through the mailbox configured in ``.Flow_Web/email_config.json``. """ import os @@ -29,6 +32,7 @@ def main(): + """Start the server web backend and serve its UI.""" os.makedirs(FLOW_WEB_DIR, exist_ok=True) app = ServerWebApp() if os.path.exists(SERVER_CONFIG_FILE): diff --git a/PyFlow/transfer_web/static/client_account.js b/PyFlow/transfer_web/static/client_account.js new file mode 100644 index 0000000..d5caf7d --- /dev/null +++ b/PyFlow/transfer_web/static/client_account.js @@ -0,0 +1,520 @@ +/* PyFlow client account UI. + * + * User info, contact search and pending contact requests for the web client. + * The client backend proxies every request to the server the TCP client is + * connected to, so this script only talks to its own backend. + * + * client_main.html sets window.WEB_USER and calls + * PyFlowClientAccount.mount({user: window.WEB_USER}); the sidebar buttons + * #contacts-btn, #requests-btn, #user-info-btn and #logout-btn drive it. + */ +(function () { + "use strict"; + + const POLL_MS = 5000; // pending-request polling interval + const state = { user: null, acceptedWhileOpen: false }; + + function esc(s) { + const div = document.createElement("div"); + div.textContent = s == null ? "" : String(s); + return div.innerHTML; + } + + async function api(path, options) { + const resp = await fetch(path, options); + let data = null; + try { + data = await resp.json(); + } catch (e) { + data = {}; + } + if (resp.status === 401) { + // The session is gone (logged out, expired or dropped by the server): + // "/" decides whether the login window has to be shown again. + location.href = "/"; + throw new Error(data.error || "login required"); + } + if (!resp.ok || data.ok === false) { + throw new Error(data.error || "HTTP " + resp.status); + } + return data; + } + + function post(path, body) { + return api(path, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + } + + function toast(text, kind) { + const el = document.createElement("div"); + el.className = "toast" + (kind ? " " + kind : ""); + el.textContent = text; + document.body.appendChild(el); + setTimeout(() => el.remove(), 3500); + } + + function openModal(html, extraClass) { + const backdrop = document.createElement("div"); + backdrop.className = "modal-backdrop"; + backdrop.innerHTML = + '
' + html + "
"; + backdrop.addEventListener("click", (e) => { + if (e.target === backdrop) backdrop.remove(); + }); + document.body.appendChild(backdrop); + return backdrop; + } + + function closeModal(backdrop) { + backdrop.remove(); + } + + /* ---------------- pending requests ---------------- */ + + async function pollRequests() { + let data; + try { + data = await api("/api/contact_requests"); + } catch (e) { + return; // offline or logged out: api() already returned to "/" + } + updateBadge(data); + if (state.acceptedWhileOpen) { + // An accepted request changed the contacts of the account, so the page + // is reloaded to show the instance list the server pushed over TCP. + state.acceptedWhileOpen = false; + location.reload(); + } + } + + function updateBadge(requests) { + const badge = document.getElementById("requests-count"); + if (!badge) return; + const count = (requests.incoming || []).length; + badge.textContent = count ? " (" + count + ")" : ""; + } + + async function answerRequest(requestId, accept) { + await post("/api/contacts/respond", { request_id: requestId, accept }); + toast(accept ? "Contact added" : "Request rejected", "ok"); + if (accept) { + state.acceptedWhileOpen = true; + pollRequests(); + } + } + + /* ---------------- shared rows ---------------- */ + + function actionButton(label, className, handler) { + const btn = document.createElement("button"); + btn.className = className; + btn.textContent = label; + if (handler) { + btn.addEventListener("click", handler); + } else { + btn.disabled = true; // the action is already done or not available + } + return btn; + } + + function accountRow(account, buttons) { + const row = document.createElement("div"); + row.className = "user-row"; + row.innerHTML = + '' + + esc(account.username) + + 'ID ' + + esc(account.user_id) + + " · " + + esc(account.email || "no email") + + ""; + buttons.forEach((btn) => row.appendChild(btn)); + return row; + } + + /* ---------------- user info ---------------- */ + + function openUserInfoModal() { + const user = state.user || {}; + const backdrop = openModal( + "

User info

" + + '
' + + '' + + esc(user.username || "unknown") + + '' + + esc(user.email || "no email") + + "" + + '' + + esc(user.role || "user") + + "
" + + '
' + + esc(user.user_id || "unknown") + + "
" + + '
' + ); + backdrop + .querySelector("#user-info-close") + .addEventListener("click", () => closeModal(backdrop)); + } + + /* ---------------- contacts ---------------- */ + + function openContactsModal() { + const backdrop = openModal( + "

Contacts

" + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + const results = backdrop.querySelector("#contact-results"); + const status = backdrop.querySelector("#contact-status"); + const searchInput = backdrop.querySelector("#contact-search"); + let pending = {}; // user id -> id of the request that account sent us + + function fail(message) { + status.className = "status-line err"; + status.textContent = message; + } + + async function search() { + status.className = "status-line"; + status.textContent = ""; + pending = {}; + try { + (await api("/api/contact_requests")).incoming.forEach((entry) => { + pending[entry.user.user_id] = entry.id; + }); + } catch (e) { + /* the search still works; an accept then reports the stale request */ + } + try { + const data = await post("/api/contacts/search", { query: searchInput.value.trim() }); + render(data.results || []); + } catch (e) { + fail(e.message); + } + } + + async function respond(userId, accept) { + const requestId = pending[userId]; + if (requestId === undefined) { + fail("This request is not pending any more."); + search(); + return; + } + try { + await answerRequest(requestId, accept); + if (accept) { + closeModal(backdrop); + } else { + search(); + } + } catch (e) { + fail(e.message); + } + } + + async function add(entry) { + try { + await post("/api/contacts/request", { user_id: entry.user_id }); + toast("Request sent to " + entry.username, "ok"); + search(); + } catch (e) { + fail(e.message); + } + } + + function render(entries) { + results.innerHTML = ""; + if (!entries.length) { + results.textContent = "No account matches that search"; + return; + } + entries.forEach((entry) => { + const buttons = []; + if (entry.relation === "none") { + buttons.push(actionButton("Add", "btn", () => add(entry))); + } else if (entry.relation === "incoming") { + buttons.push(actionButton("Accept", "btn", () => respond(entry.user_id, true))); + buttons.push(actionButton("Reject", "btn btn-danger", () => respond(entry.user_id, false))); + } else if (entry.relation === "outgoing") { + buttons.push(actionButton("Sent", "btn btn-ghost", null)); + } else { + buttons.push(actionButton("Contact", "btn btn-ghost", null)); + } + results.appendChild(accountRow(entry, buttons)); + }); + } + + backdrop + .querySelector("#contacts-close") + .addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#contact-search-btn").addEventListener("click", search); + searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + search(); + } + }); + } + + /* ---------------- contact requests ---------------- */ + + function openRequestsModal() { + const backdrop = openModal( + "

Contact requests

" + + '
' + + '
' + + '
' + ); + const list = backdrop.querySelector("#requests-list"); + const status = backdrop.querySelector("#requests-status"); + + function fail(message) { + status.className = "status-line err"; + status.textContent = message; + } + + function render(requests) { + const incoming = requests.incoming || []; + const outgoing = requests.outgoing || []; + status.className = "status-line"; + status.textContent = ""; + list.innerHTML = ""; + incoming.forEach((entry) => { + list.appendChild( + accountRow(entry.user, [ + actionButton("Accept", "btn", () => respond(entry, true)), + actionButton("Reject", "btn btn-danger", () => respond(entry, false)), + ]) + ); + }); + outgoing.forEach((entry) => { + list.appendChild(accountRow(entry.user, [actionButton("Sent", "btn btn-ghost", null)])); + }); + if (!incoming.length && !outgoing.length) { + list.textContent = "No pending requests"; + } + } + + async function respond(entry, accept) { + try { + await answerRequest(entry.id, accept); + if (accept) { + closeModal(backdrop); + } else { + refresh(); + } + } catch (e) { + fail(e.message); + } + } + + async function refresh() { + try { + render(await api("/api/contact_requests")); + } catch (e) { + fail(e.message); + } + } + + backdrop + .querySelector("#requests-close") + .addEventListener("click", () => closeModal(backdrop)); + refresh(); + } + + /* ---------------- server "ftp" share (not FTP: native protocol transfers) ---------------- */ + + // Selected share-relative paths survive folder navigation, so a selection can + // span folders; the checkbox on the left of every row is the only state. + const ftpState = { path: "", selected: new Set(), listing: null }; + + function fmtSize(n) { + if (n == null || isNaN(n)) return ""; + const units = ["B", "KB", "MB", "GB", "TB"]; + let v = Number(n); + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i++; + } + return (v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)) + " " + units[i]; + } + + function ftpJoin(base, name) { + return base ? base + "/" + name : name; + } + + function ftpEntryRow(entry, render, openFolder) { + const rel = ftpJoin(ftpState.path, entry.name); + const row = document.createElement("div"); + row.className = "ftp-row" + (entry.dir ? " dir" : "") + (ftpState.selected.has(rel) ? " picked" : ""); + const box = document.createElement("input"); + box.type = "checkbox"; + box.checked = ftpState.selected.has(rel); + box.title = "Select " + entry.name; + row.appendChild(box); + const icon = document.createElement("span"); + icon.className = "ftp-icon"; + icon.innerHTML = entry.dir ? "📁" : "📄"; + row.appendChild(icon); + const name = document.createElement("span"); + name.className = "ftp-name"; + name.textContent = entry.name; + row.appendChild(name); + const meta = document.createElement("span"); + meta.className = "ftp-meta"; + meta.textContent = entry.dir ? "folder" : fmtSize(entry.size); + row.appendChild(meta); + const when = document.createElement("span"); + when.className = "ftp-meta"; + when.textContent = new Date(entry.mtime * 1000).toLocaleString(); + row.appendChild(when); + + // Windows-explorer conventions: a single click toggles the checkbox of the + // row, a double click opens a folder and is ignored on a file (the two + // clicks of the double click cancel each other out). + function toggle() { + if (ftpState.selected.has(rel)) ftpState.selected.delete(rel); + else ftpState.selected.add(rel); + box.checked = ftpState.selected.has(rel); + row.classList.toggle("picked", box.checked); + render(); + } + row.addEventListener("click", (e) => { + if (e.target !== box) toggle(); + }); + row.addEventListener("dblclick", () => { + if (!entry.dir) return; // double-clicking a file does nothing + openFolder(rel); + }); + return row; + } + + // Refresh the download button of a modal from the current selection. + function updateFtpFooter(backdrop) { + const downloadBtn = backdrop.querySelector("#ftp-download"); + if (!downloadBtn) return; + downloadBtn.textContent = "Download selected (" + ftpState.selected.size + ")"; + downloadBtn.disabled = ftpState.selected.size === 0; + } + + function renderFtpModal(backdrop) { + const listing = ftpState.listing || { entries: [], path: "", parent: null }; + const list = backdrop.querySelector("#ftp-list"); + backdrop.querySelector("#ftp-path").textContent = "/" + (listing.path || ""); + backdrop.querySelector("#ftp-up").disabled = + listing.parent === null || listing.parent === undefined; + updateFtpFooter(backdrop); + list.innerHTML = ""; + if (!listing.entries.length) { + const empty = document.createElement("div"); + empty.className = "empty-hint"; + empty.textContent = "This folder is empty"; + list.appendChild(empty); + return; + } + const refreshFooter = () => updateFtpFooter(backdrop); + const openFolder = (rel) => { + ftpState.path = rel; + loadFtpFolder(backdrop); + }; + listing.entries.forEach((entry) => + list.appendChild(ftpEntryRow(entry, refreshFooter, openFolder)) + ); + } + + async function loadFtpFolder(backdrop) { + const modal = backdrop || document.querySelector(".modal-backdrop"); + try { + const data = await post("/api/ftp/list", { path: ftpState.path }); + ftpState.listing = data.listing || { entries: [], path: ftpState.path, parent: null }; + ftpState.path = ftpState.listing.path || ""; + if (modal) renderFtpModal(modal); + } catch (e) { + toast('Cannot open the server "ftp" server: ' + e.message, "err"); + if (modal) closeModal(modal); + } + } + + function openFtpModal() { + ftpState.path = ""; + ftpState.selected = new Set(); + const backdrop = openModal( + "

Server "ftp"

" + + '
Files and folders below live on the server. Tick what you want ' + + "and download it over the PyFlow transfer protocol: click selects, double click opens " + + "a folder (a file ignores the double click).
" + + '
' + + '/' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + ); + backdrop.querySelector("#ftp-close").addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#ftp-up").addEventListener("click", () => { + const parent = ftpState.listing ? ftpState.listing.parent : null; + if (parent === null || parent === undefined) return; + ftpState.path = parent; + loadFtpFolder(backdrop); + }); + backdrop.querySelector("#ftp-refresh").addEventListener("click", () => loadFtpFolder(backdrop)); + backdrop.querySelector("#ftp-download").addEventListener("click", async () => { + const paths = Array.from(ftpState.selected); + if (!paths.length) return; + const destination = backdrop.querySelector("#ftp-dest").value.trim(); + try { + const data = await post("/api/ftp/download", { paths, destination }); + closeModal(backdrop); + toast("Download started (" + (data.started || 0) + " item(s))", "ok"); + } catch (e) { + toast("Download failed: " + e.message, "err"); + } + }); + loadFtpFolder(backdrop); + } + + /* ---------------- session ---------------- */ + + async function logout() { + try { + await api("/api/logout", { method: "POST" }); + } catch (e) { + /* the session goes away either way */ + } + location.href = "/"; + } + + window.PyFlowClientAccount = { + mount(options) { + state.user = (options && options.user) || window.WEB_USER || {}; + const contactsBtn = document.getElementById("contacts-btn"); + if (contactsBtn) contactsBtn.addEventListener("click", openContactsModal); + const requestsBtn = document.getElementById("requests-btn"); + if (requestsBtn) requestsBtn.addEventListener("click", openRequestsModal); + const userInfoBtn = document.getElementById("user-info-btn"); + if (userInfoBtn) userInfoBtn.addEventListener("click", openUserInfoModal); + const ftpBtn = document.getElementById("ftp-btn"); + if (ftpBtn) ftpBtn.addEventListener("click", openFtpModal); + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) logoutBtn.addEventListener("click", logout); + pollRequests(); + setInterval(pollRequests, POLL_MS); + }, + openFtp: openFtpModal, + logout: logout, + }; +})(); diff --git a/PyFlow/transfer_web/static/common.css b/PyFlow/transfer_web/static/common.css index 3cbbad6..de2f04b 100644 --- a/PyFlow/transfer_web/static/common.css +++ b/PyFlow/transfer_web/static/common.css @@ -59,6 +59,23 @@ a { color: var(--accent); } font-size: 13px; } +/* ---------- server account (card topbar, credential warning, users) ---------- */ + +.card-topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + font-size: 12px; + color: var(--text-dim); +} + +.card-topbar .btn { + padding: 5px 10px; + font-size: 12px; +} + .form-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -365,6 +382,47 @@ a { color: var(--accent); } .modal .field { margin-bottom: 12px; } +/* ---------- default-credential warning / user list ---------- */ + +.modal.warn { + border-color: var(--err); + box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.25), 0 16px 50px rgba(0, 0, 0, 0.5); +} + +.modal .warn-title { + color: var(--err); + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; +} + +.modal .warn-body { + font-size: 13px; + line-height: 1.7; +} + +.modal .warn-body p + p { margin-top: 10px; } + +.user-list { margin-bottom: 14px; } + +.user-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--border); +} + +.user-row .name { flex: 1; font-size: 13px; } +.user-row .role { font-size: 11px; color: var(--text-dim); } +.user-row .btn { padding: 5px 10px; font-size: 12px; } + +.user-row .name .meta { + display: block; + font-size: 11px; + color: var(--text-dim); +} + .drop-zone { border: 2px dashed var(--border); border-radius: 8px; @@ -399,6 +457,35 @@ a { color: var(--accent); } .ext-entry .info .name { font-size: 13px; } .ext-entry .info .cmd { font-size: 11px; color: var(--text-dim); } +/* "ftp" share explorer (server console + client selection dialog) */ +.ftp-bar { display: flex; align-items: center; gap: 8px; margin: 8px 0 6px; } +.ftp-bar .spacer { flex: 1; } +.ftp-path { font-size: 12px; color: var(--text-dim); } +.ftp-list { + max-height: 46vh; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 8px; +} +.ftp-row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + border-bottom: 1px solid var(--border); + font-size: 13px; +} +.ftp-row:last-child { border-bottom: none; } +.ftp-row:hover { background: var(--bg-3); } +.ftp-row.dir { cursor: pointer; } +.ftp-row .ftp-icon { width: 18px; text-align: center; } +.ftp-row .ftp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ftp-row .ftp-meta { font-size: 11px; color: var(--text-dim); white-space: nowrap; } +.ftp-row input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; } +.ftp-row.picked { background: rgba(90, 160, 255, 0.12); } +.ftp-footer { display: flex; align-items: center; gap: 10px; margin-top: 14px; } +.ftp-footer .spacer { flex: 1; } + select { width: 100%; background: var(--bg); diff --git a/PyFlow/transfer_web/static/common.js b/PyFlow/transfer_web/static/common.js index 64817cd..bd91a32 100644 --- a/PyFlow/transfer_web/static/common.js +++ b/PyFlow/transfer_web/static/common.js @@ -5,7 +5,17 @@ "use strict"; const MODE = window.WEB_MODE || "client"; + // Server pages are role-gated: configuration, extensions and user management + // are administrator-only (the backend enforces this as well). The client web + // UI belongs to the user running the client backend. + const IS_ADMIN = MODE === "client" || window.WEB_ROLE === "admin"; const $ = (id) => document.getElementById(id); + // Bind a handler only when the element exists: administrator-only controls + // are absent from the server page markup for regular users. + const on = (id, event, handler) => { + const el = $(id); + if (el) el.addEventListener(event, handler); + }; const state = { serverInfo: null, @@ -36,6 +46,12 @@ } catch (e) { data = {}; } + + if (resp.status === 401) { + // the session is gone: back to the login page + location.href = "/"; + throw new Error("login required"); + } if (!resp.ok || data.ok === false) { throw new Error(data.error || ("HTTP " + resp.status)); } @@ -97,7 +113,7 @@ let shown = 0; state.clients.forEach((c) => { - // a client never lists itself; the server is never in the client list + // a client never lists itself; the client list holds only contacts if (MODE === "client" && isSelf(c)) return; shown++; const el = document.createElement("div"); @@ -108,10 +124,14 @@ state.target[0] === c.ip && state.target[1] === c.port; el.className = "instance" + (active ? " active" : ""); + // Server-side accounts (web clients) carry a name; a bare instance does not. + const name = c.username ? c.username : key; + const tag = c.user_id ? c.user_id : "client"; el.innerHTML = '' + - '' + esc(key) + "" + - 'client'; + '' + esc(name) + "" + + '' + esc(tag) + ""; + el.title = key + (c.email ? " · " + c.email : ""); el.addEventListener("click", () => selectTarget([c.ip, c.port])); list.appendChild(el); }); @@ -119,7 +139,8 @@ const empty = document.createElement("div"); empty.className = "empty-hint"; empty.style.padding = "16px 8px"; - empty.textContent = "No clients connected"; + empty.textContent = + MODE === "client" ? "No contact is connected" : "No clients connected"; list.appendChild(empty); } } @@ -446,7 +467,9 @@ btn.title = entry.name + " (" + entry.command + ")"; btn.textContent = entry.icon; btn.addEventListener("click", () => openRunExtensionModal(entry)); - bar.insertBefore(btn, $("plus-btn")); + const anchor = $("plus-btn"); + if (anchor) bar.insertBefore(btn, anchor); + else bar.appendChild(btn); }); } @@ -514,6 +537,7 @@ /* ---------------- init ---------------- */ async function loadExtensions() { + if (!IS_ADMIN) return; // extension protocols are administrator-only try { state.extensions = (await api("/api/extensions_ui")).extensions || []; renderExtensionIcons(); @@ -523,21 +547,22 @@ } function init() { - $("send-btn").addEventListener("click", sendMessage); - $("input").addEventListener("keydown", (e) => { + const input = $("input"); + on("send-btn", "click", sendMessage); + input.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); - $("input").addEventListener("input", () => { - $("input").style.height = "auto"; - $("input").style.height = Math.min($("input").scrollHeight, 160) + "px"; + input.addEventListener("input", () => { + input.style.height = "auto"; + input.style.height = Math.min(input.scrollHeight, 160) + "px"; }); - $("file-btn").addEventListener("click", () => openFileModal(false)); - $("folder-btn").addEventListener("click", () => openFileModal(true)); - $("plus-btn").addEventListener("click", openAddProtocolModal); - $("reload-btn").addEventListener("click", async () => { + on("file-btn", "click", () => openFileModal(false)); + on("folder-btn", "click", () => openFileModal(true)); + on("plus-btn", "click", openAddProtocolModal); + on("reload-btn", "click", async () => { try { if (MODE === "client") { await api("/api/sync_clients", { method: "POST" }); @@ -548,7 +573,7 @@ toast("Reload failed: " + e.message, "err"); } }); - $("add-ext-btn").addEventListener("click", openExtensionManager); + on("add-ext-btn", "click", openExtensionManager); selectTarget("server"); loadExtensions(); refreshStatus(); diff --git a/PyFlow/transfer_web/static/server_account.js b/PyFlow/transfer_web/static/server_account.js new file mode 100644 index 0000000..959129b --- /dev/null +++ b/PyFlow/transfer_web/static/server_account.js @@ -0,0 +1,456 @@ +/* PyFlow server account UI. + * + * Default-credential warning, credential change and user management for the + * server web backend (/api/login, /api/account, /api/users). Independent of + * common.js, so both the server status page and the startup-configuration page + * can mount it. + * + * Templates call PyFlowAccount.mount({role, username, mustChange}) and add the + * optional #users-btn / #logout-btn buttons. + */ +(function () { + "use strict"; + + const state = { role: "user", username: "", user_id: "", email: "", mustChange: false }; + const MIN_PASSWORD_LENGTH = 8; + + function esc(s) { + const div = document.createElement("div"); + div.textContent = s == null ? "" : String(s); + return div.innerHTML; + } + + function escAttr(s) { + return esc(s).replace(/"/g, """); + } + + async function api(path, options) { + const resp = await fetch(path, options); + let data = null; + try { + data = await resp.json(); + } catch (e) { + data = {}; + } + if (resp.status === 401) { + // No session any more (logged out or the account was removed). + location.href = "/"; + throw new Error("login required"); + } + if (!resp.ok || data.ok === false) { + throw new Error(data.error || "HTTP " + resp.status); + } + return data; + } + + function toast(text, kind) { + const el = document.createElement("div"); + el.className = "toast" + (kind ? " " + kind : ""); + el.textContent = text; + document.body.appendChild(el); + setTimeout(() => el.remove(), 3500); + } + + function openModal(html, extraClass) { + const backdrop = document.createElement("div"); + backdrop.className = "modal-backdrop"; + backdrop.innerHTML = + '
' + html + "
"; + backdrop.addEventListener("click", (e) => { + if (e.target === backdrop) backdrop.remove(); + }); + document.body.appendChild(backdrop); + return backdrop; + } + + function closeModal(backdrop) { + backdrop.remove(); + } + + /* ---------------- change own credentials ---------------- */ + + function openCredentialsModal() { + const backdrop = openModal( + "

Change your credentials

" + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
" + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + const status = backdrop.querySelector("#acc-status"); + const fail = (message) => { + status.className = "status-line err"; + status.textContent = message; + }; + backdrop.querySelector("#acc-cancel").addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#acc-save").addEventListener("click", async () => { + const current = backdrop.querySelector("#acc-current").value; + const username = backdrop.querySelector("#acc-username").value.trim(); + const email = backdrop.querySelector("#acc-email").value.trim(); + const password = backdrop.querySelector("#acc-password").value; + const confirm = backdrop.querySelector("#acc-confirm").value; + if (!current) return fail("Enter your current password."); + if (!username) return fail("Enter a username."); + if (password.length < MIN_PASSWORD_LENGTH) { + return fail("The new password must be at least " + MIN_PASSWORD_LENGTH + " characters."); + } + if (password !== confirm) return fail("The two passwords do not match."); + try { + await api("/api/account", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ current_password: current, username, password, email }), + }); + closeModal(backdrop); + toast("Credentials updated", "ok"); + setTimeout(() => location.reload(), 600); + } catch (e) { + fail(e.message); + } + }); + } + + /* ---------------- user management (administrators) ---------------- */ + + function openUsersModal() { + const backdrop = openModal( + "

Users

" + + '
' + + '
' + + '
' + + '
' + + '
' + + '
" + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + const list = backdrop.querySelector("#users-list"); + const status = backdrop.querySelector("#users-status"); + const fail = (message) => { + status.className = "status-line err"; + status.textContent = message; + }; + + function render(users) { + list.innerHTML = ""; + users.forEach((u) => { + const row = document.createElement("div"); + row.className = "user-row"; + row.innerHTML = + '' + + esc(u.username) + + (u.username === state.username ? " (you)" : "") + + '' + + esc(u.email || "no email") + + " · ID " + + esc(u.user_id) + + '' + + esc(u.role) + + ""; + if (u.username !== state.username) { + const removeBtn = document.createElement("button"); + removeBtn.className = "btn btn-danger"; + removeBtn.textContent = "Remove"; + removeBtn.addEventListener("click", async () => { + try { + const data = await api("/api/users/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: u.username }), + }); + status.className = "status-line"; + status.textContent = ""; + render(data.users || []); + toast("User removed", "ok"); + } catch (e) { + fail(e.message); + } + }); + row.appendChild(removeBtn); + } + list.appendChild(row); + }); + if (!users.length) list.textContent = "No users"; + } + + async function refresh() { + try { + render((await api("/api/users")).users || []); + } catch (e) { + fail(e.message); + } + } + + backdrop.querySelector("#users-close-btn").addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#user-add-btn").addEventListener("click", async () => { + const username = backdrop.querySelector("#user-username").value.trim(); + const email = backdrop.querySelector("#user-email").value.trim(); + const password = backdrop.querySelector("#user-password").value; + const role = backdrop.querySelector("#user-role").value; + if (!username) return fail("Enter a username."); + if (!email) return fail("Enter the email address of the account."); + if (password.length < MIN_PASSWORD_LENGTH) { + return fail("The password must be at least " + MIN_PASSWORD_LENGTH + " characters."); + } + try { + const data = await api("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, email, password, role }), + }); + status.className = "status-line"; + status.textContent = ""; + backdrop.querySelector("#user-username").value = ""; + backdrop.querySelector("#user-email").value = ""; + backdrop.querySelector("#user-password").value = ""; + render(data.users || []); + toast("User added", "ok"); + } catch (e) { + fail(e.message); + } + }); + + refresh(); + } + + /* ---------------- default-credential warning ---------------- */ + + function openDefaultCredentialsWarning() { + const backdrop = openModal( + '
⚠ The default administrator credentials are still in use
' + + '
' + + "

This server is administered with the default account: username admin, " + + "password admin. Anyone who can reach the server can log in as " + + "administrator with these credentials.

" + + "

Change the admin username and password before this server goes " + + "online on a public network.

" + + "
" + + '
" + + '
', + "warn" + ); + backdrop.querySelector("#warn-change-btn").addEventListener("click", () => { + closeModal(backdrop); + openCredentialsModal(); + }); + backdrop.querySelector("#warn-later-btn").addEventListener("click", () => closeModal(backdrop)); + } + + + /* ---------------- "ftp" server (not FTP: the protocol's own transfers) ---------------- */ + + // The brand of the feature is the literal quoted "ftp": this is not the FTP + // protocol, it browses one folder of this host and hands the chosen entries + // to clients over the PyFlow file transfer commands. + const FTP_BUTTON_ADD = '+ "ftp" server'; + const FTP_BUTTON_VIEW = '"ftp" server status'; + + function fmtSize(n) { + if (n == null || isNaN(n)) return ""; + const units = ["B", "KB", "MB", "GB", "TB"]; + let v = Number(n); + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i++; + } + return (v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)) + " " + units[i]; + } + + function ftpButton() { + return document.getElementById("ftp-btn"); + } + + async function refreshFtpButton() { + const btn = ftpButton(); + if (!btn) return null; + try { + const data = await api("/api/ftp"); + const shared = !!data.shared; + btn.textContent = shared ? FTP_BUTTON_VIEW : FTP_BUTTON_ADD; + return data.root || null; + } catch (e) { + return null; + } + } + + function openFtpAddDialog(currentRoot) { + const backdrop = openModal( + "

Add an "ftp" server

" + + '
This is not FTP: the chosen folder is served to connected ' + + "clients over the PyFlow file transfer protocol.
" + + '
' + + '
' + + '
' + + '
' + + '
' + ); + const status = backdrop.querySelector("#ftp-status"); + backdrop.querySelector("#ftp-add-cancel").addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#ftp-add-btn").addEventListener("click", async () => { + const path = backdrop.querySelector("#ftp-path").value.trim(); + if (!path) { + status.className = "status-line err"; + status.textContent = "Enter a folder path"; + return; + } + try { + const data = await api("/api/ftp/add", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path }), + }); + closeModal(backdrop); + await refreshFtpButton(); + toast('"ftp" server added: ' + data.root, "ok"); + openFtpBrowser(""); + } catch (e) { + status.className = "status-line err"; + status.textContent = e.message; + } + }); + } + + // Share-relative path of one child entry of ``base``. + function ftpJoin(base, name) { + return base ? base + "/" + name : name; + } + + function ftpRow(entry, relBase, openFolder) { + const row = document.createElement("div"); + row.className = "ftp-row" + (entry.dir ? " dir" : ""); + row.innerHTML = + '' + (entry.dir ? "📁" : "📄") + "" + + '' + esc(entry.name) + "" + + '' + (entry.dir ? "folder" : fmtSize(entry.size)) + "" + + '' + new Date(entry.mtime * 1000).toLocaleString() + ""; + if (entry.dir) { + row.title = "Open " + entry.name; + row.addEventListener("click", () => openFolder(ftpJoin(relBase, entry.name))); + } + return row; + } + + async function openFtpBrowser(relPath) { + let data; + try { + data = await api("/api/ftp/list?path=" + encodeURIComponent(relPath || "")); + } catch (e) { + toast("Cannot list the shared folder: " + e.message, "err"); + return; + } + const listing = data.listing || { entries: [] }; + const backdrop = openModal( + "

"ftp" server

" + + '
' + + '' + + esc("/" + (listing.path || "")) + + "" + + '' + + '' + + '
' + + '
Shared host folder: ' + + esc(data.root || "") + + "
" + + '
' + ); + const list = backdrop.querySelector("#ftp-list"); + + function render() { + list.innerHTML = ""; + if (!listing.entries.length) { + const empty = document.createElement("div"); + empty.className = "empty-hint"; + empty.textContent = "This folder is empty"; + list.appendChild(empty); + return; + } + listing.entries.forEach((entry) => + list.appendChild( + ftpRow(entry, listing.path, (rel) => { + closeModal(backdrop); + openFtpBrowser(rel); + }) + ) + ); + } + + render(); + const upBtn = backdrop.querySelector("#ftp-up"); + if (listing.parent === null || listing.parent === undefined) { + upBtn.disabled = true; + } else { + upBtn.addEventListener("click", () => { + closeModal(backdrop); + openFtpBrowser(listing.parent); + }); + } + backdrop.querySelector("#ftp-close").addEventListener("click", () => closeModal(backdrop)); + backdrop.querySelector("#ftp-change").addEventListener("click", () => { + closeModal(backdrop); + openFtpAddDialog(data.root || ""); + }); + } + + async function openFtpServer() { + const root = await refreshFtpButton(); + if (root) openFtpBrowser(""); + else openFtpAddDialog(""); + } + + /* ---------------- session ---------------- */ + + async function logout() { + try { + await api("/api/logout", { method: "POST" }); + } catch (e) { + /* the session is gone either way */ + } + location.href = "/"; + } + + window.PyFlowAccount = { + mount(options) { + Object.assign(state, options || {}); + const usersBtn = document.getElementById("users-btn"); + if (usersBtn) usersBtn.addEventListener("click", openUsersModal); + const ftpBtn = document.getElementById("ftp-btn"); + if (ftpBtn) { + ftpBtn.addEventListener("click", openFtpServer); + refreshFtpButton(); + } + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) logoutBtn.addEventListener("click", logout); + if (state.mustChange) openDefaultCredentialsWarning(); + }, + openCredentials: openCredentialsModal, + openUsers: openUsersModal, + openFtpServer: openFtpServer, + logout: logout, + }; +})(); diff --git a/PyFlow/transfer_web/web_backend/mail_service.py b/PyFlow/transfer_web/web_backend/mail_service.py new file mode 100644 index 0000000..49233d6 --- /dev/null +++ b/PyFlow/transfer_web/web_backend/mail_service.py @@ -0,0 +1,236 @@ +"""SMTP delivery of account verification codes for the web tool. + +The administrator configures the outgoing mailbox from the server's +startup-configuration page. The settings are validated with a real SMTP login +and only then written to ``.Flow_Web/email_config.json``; while no validated +configuration is present the service refuses to send, so a server can never +silently drop verification codes. +""" + +import json +import os +import smtplib +import ssl +import threading +import traceback +from email.message import EmailMessage + +WEB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FLOW_WEB_DIR = os.path.join(WEB_ROOT, ".Flow_Web") +MAIL_CONFIG_FILE = os.path.join(FLOW_WEB_DIR, "email_config.json") + +ENCRYPTION_MODES = ("ssl", "starttls", "none") +DEFAULT_ENCRYPTION = "ssl" +SMTP_TIMEOUT = 10 +MAX_PORT = 65535 + +# Human-readable purpose labels used in the mail body. +PURPOSE_LABELS = { + "register": "account registration", + "login": "login", + "reset_password": "password change", +} + + +def normalize_config(config): + """Validate untrusted SMTP settings and return them in canonical form. + + Args: + config (dict | None): Settings with ``host``, ``port``, ``username``, + ``password``, ``from`` and ``encryption`` keys. ``from`` defaults to + the username and ``encryption`` to ``DEFAULT_ENCRYPTION``. + + Returns: + dict: The same settings with a string host, an integer port, a lowercase + encryption mode and an explicit sender. + + Raises: + ValueError: If a setting is missing, malformed or out of range. + """ + config = config or {} + host = str(config.get("host") or "").strip() + username = str(config.get("username") or "").strip() + password = str(config.get("password") or "") + sender = str(config.get("from") or "").strip() or username + encryption = str(config.get("encryption") or DEFAULT_ENCRYPTION).strip().lower() + if not host: + raise ValueError("enter the SMTP server address (host)") + if encryption not in ENCRYPTION_MODES: + raise ValueError("the encryption must be one of " + ", ".join(ENCRYPTION_MODES)) + if not username: + raise ValueError("enter the mailbox account (username)") + if not password: + raise ValueError("enter the authorization code or password of the mailbox") + if "@" not in sender: + raise ValueError("enter the sender address (from)") + try: + port = int(config.get("port")) + except (TypeError, ValueError) as e: + raise ValueError("enter a valid SMTP port") from e + if not 1 <= port <= MAX_PORT: + raise ValueError(f"the SMTP port must be between 1 and {MAX_PORT}") + return { + "host": host, + "port": port, + "username": username, + "password": password, + "from": sender, + "encryption": encryption, + } + + +class MailService: + """SMTP client delivering verification codes. + + The stored configuration is read on construction, so a validated mailbox is + available again after a restart. + """ + + def __init__(self, config_path=None): + """Create the service and load the stored configuration. + + Args: + config_path (str | None): JSON file holding the SMTP settings; + defaults to ``.Flow_Web/email_config.json``. + """ + self.config_path = config_path or MAIL_CONFIG_FILE + self._lock = threading.Lock() + self._config = None + self._load() + + # ------------------------------------------------------------- internals + + def _load(self): + """Read a previously validated configuration, disabling on any problem.""" + if not os.path.exists(self.config_path): + return + try: + with open(self.config_path, "r", encoding="utf-8") as f: + self._config = normalize_config(json.load(f)) + except Exception as e: + traceback.print_exc() + self._config = None + print(f"verification email: ignoring {self.config_path}: {e}") + + def _connect(self, config): + """Open an authenticated SMTP connection with the configured encryption.""" + context = ssl.create_default_context() + if config["encryption"] == "ssl": + smtp = smtplib.SMTP_SSL( + config["host"], config["port"], timeout=SMTP_TIMEOUT, context=context + ) + else: + smtp = smtplib.SMTP(config["host"], config["port"], timeout=SMTP_TIMEOUT) + if config["encryption"] == "starttls": + smtp.starttls(context=context) + try: + smtp.login(config["username"], config["password"]) + except Exception: + smtp.close() + raise + return smtp + + def _close(self, smtp): + """Close an SMTP connection, ignoring a peer that already hung up.""" + try: + smtp.quit() + except Exception: + try: + smtp.close() + except Exception: + pass + + # ----------------------------------------------------------------- public + + def is_enabled(self): + """Report whether a validated configuration lets codes be sent. + + Returns: + bool: True when the SMTP settings were validated and stored. + """ + return self._config is not None + + def get_config(self): + """Return the validated SMTP settings. + + Returns: + dict | None: Copy of the stored settings, or ``None`` while the + service is not configured. + """ + with self._lock: + return dict(self._config) if self._config else None + + def configure(self, config): + """Validate SMTP settings, store them and start the sending service. + + Args: + config (dict): Settings accepted by `normalize_config`. + + Returns: + dict: The validated settings as stored. + + Raises: + ValueError: If a setting is missing or malformed, or the server + refuses the connection or the login. + """ + normalized = normalize_config(config) + try: + smtp = self._connect(normalized) + except Exception as e: + raise ValueError(f"the SMTP settings were rejected: {e}") from e + self._close(smtp) + directory = os.path.dirname(self.config_path) + if directory: + os.makedirs(directory, exist_ok=True) + tmp = self.config_path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(normalized, f, indent=4, ensure_ascii=False) + os.replace(tmp, self.config_path) + try: + os.chmod(self.config_path, 0o600) + except OSError: + pass + with self._lock: + self._config = normalized + return dict(normalized) + + def send_code(self, to_address, code, purpose, expires_in): + """Send one verification code by email. + + Args: + to_address (str): Recipient address. + code (str): Verification code to deliver. + purpose (str): "register", "login" or "reset_password"; decides the + wording of the message. + expires_in (int): Code lifetime in seconds, stated in the message. + + Raises: + ValueError: If the service is not configured, or the message cannot + be delivered. + """ + with self._lock: + config = dict(self._config) if self._config else None + if config is None: + raise ValueError( + "the verification email service is not configured; ask the server " + "administrator to set it up" + ) + label = PURPOSE_LABELS.get(purpose, purpose) + message = EmailMessage() + message["Subject"] = "PyFlow verification code" + message["From"] = config["from"] + message["To"] = to_address + message.set_content( + f"Your PyFlow verification code for {label} is:\n\n" + f" {code}\n\n" + f"The code is valid for {max(1, int(expires_in) // 60)} minutes and can be " + "used once. If you did not request it, ignore this message.\n" + ) + try: + smtp = self._connect(config) + try: + smtp.send_message(message) + finally: + self._close(smtp) + except Exception as e: + raise ValueError(f"sending the verification code to {to_address} failed: {e}") from e diff --git a/PyFlow/transfer_web/web_backend/server_backend.py b/PyFlow/transfer_web/web_backend/server_backend.py index 89c4292..b86736a 100644 --- a/PyFlow/transfer_web/web_backend/server_backend.py +++ b/PyFlow/transfer_web/web_backend/server_backend.py @@ -21,10 +21,31 @@ clients) are captured on the TCP server's receive threads through ``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` APIs, queued here, and polled by the frontend via ``/api/events``. + +Authentication: anonymous visitors get a white landing page (the server +addresses, a login button, a registration button and a password-reset button); +the configuration and status pages need a session. Accounts live in the SQLite +store ``.Flow_Web/flow_web.db`` (see `user_database`); the first run seeds the +``admin``/``admin`` administrator and the frontend warns on every login until +those default credentials are changed. Registration, password reset and +code-based client logins are delivered by the mailbox configured from the +startup-configuration page (see `mail_service`). + +Client accounts: the client web frontend logs in through ``/api/client_login`` +with a username/email, the account password *and* a mailed verification code — +both factors are required — and receives a session token. Re-entering the +client web UI replays the saved credentials and token through +``/api/client_verify``, which only accepts them while they still open that +account. The token binds the TCP connection the client opens (``/web_bind``) to +that account, and the instance list pushed to a client is limited to its +contacts, so accounts that never exchanged a contact request cannot see each +other. """ +import functools import json import os +import secrets import shlex import socket import subprocess @@ -33,11 +54,18 @@ import time import traceback -from flask import Flask, jsonify, render_template, request +from flask import Flask, jsonify, redirect, render_template, request, session from PyFlow import add_extension from PyFlow import forward_extension_tcp from PyFlow.network_api.connect_tcp import TCP_Server_Base +from PyFlow.transfer_web.web_backend.mail_service import MailService +from PyFlow.transfer_web.web_backend.user_database import ( + DEFAULT_ADMIN_PASSWORD, + DEFAULT_ADMIN_USERNAME, + UserDatabase, + mask_email, +) WEB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) FLOW_WEB_DIR = os.path.join(WEB_ROOT, ".Flow_Web") @@ -49,6 +77,15 @@ DEFAULT_WEB_PORT = 5000 +# Account store and verification-mail service: both keep their state in +# .Flow_Web (flow_web.db and email_config.json). +SECRET_KEY_FILE = os.path.join(FLOW_WEB_DIR, "web_secret_key") + +# TCP command a client sends to bind its connection to its account, and the +# acknowledgement carrying the address the server sees for that connection. +BIND_COMMAND = "/web_bind" +BIND_OK_COMMAND = "/web_bind_ok" + # Ordered (key, label, type, default, help) for every TCP_Server_Base # parameter shown in the startup-configuration UI. SERVER_PARAM_FIELDS = [ @@ -83,6 +120,15 @@ ("is_enable_encrypto", "Enable encryption", "bool", True, "RSA-encrypt the TCP channel."), ("is_custom_keys", "Custom keys", "text", "", "Optional [pub_key_path, pvt_key_path] pair."), ("max_mem_buff", "Max memory buffer (MB)", "number", 2048, "In-memory transfer buffer in MB."), + ( + "is_asynic_clients_io", + "Asyncio clients io", + "bool", + False, + "Serve clients with asyncio coroutines; max_clients is then ignored.", + ), + ("is_debug", "Debug log", "bool", False, "Log execution-process lines as well."), + ("is_print_log", "Print log", "bool", True, "Log at all; False silences the instance."), ] # Web-only settings (not TCP_Server_Base parameters). @@ -90,6 +136,109 @@ ("web_port", "Web port", "number", DEFAULT_WEB_PORT, "Port of this web backend (clients query it)."), ] +# The web "ftp" share is not FTP: it browses one folder on the server host and +# hands selected entries to clients over the protocol's native /file and +# /file_folder transfers. The shared folder is kept in the startup config +# (``web.ftp_root``), so a restart keeps serving it. +FTP_LIST_COMMAND = "/ftp_list" +FTP_GET_COMMAND = "/ftp_get" +FTP_LIST_OK_COMMAND = "/ftp_list_ok" +FTP_GET_OK_COMMAND = "/ftp_get_ok" +FTP_ERROR_COMMAND = "/ftp_error" + + +def _ftp_resolve(root, rel_path): + """Resolve one share-relative path, refusing anything outside ``root``. + + Args: + root (str): Absolute shared folder. + rel_path (str): Path relative to the share; "" is the share itself. + + Returns: + tuple: ``(absolute_path, clean_relative_path)``. + + Raises: + ValueError: If the path is absolute or escapes the shared folder. + """ + raw = (rel_path or "").strip() + rel = raw.replace("\\", "/") + # "/etc" is drive-relative on Windows (os.path.isabs is False there), so a + # leading separator and any drive prefix are refused on top of isabs + if os.path.isabs(raw) or rel.startswith("/") or os.path.splitdrive(raw)[0]: + raise ValueError("path must be relative to the shared folder") + rel = rel.strip("/") + if ".." in rel.split("/"): + raise ValueError("path must stay inside the shared folder") + root_real = os.path.realpath(root) + target = os.path.realpath(os.path.join(root_real, rel)) if rel else root_real + if target != root_real and not target.startswith(root_real + os.sep): + raise ValueError("path must stay inside the shared folder") + return target, rel + + +def _ftp_listing(root, rel_path): + """Build the listing of one folder inside the shared root. + + Args: + root (str): Absolute shared folder. + rel_path (str): Folder to list, relative to the root ("" is the root). + + Returns: + dict: ``{"path", "parent", "entries"}``; every entry is + ``{"name", "dir", "size", "mtime"}``, folders first, then files, + each group sorted by name. + + Raises: + ValueError: If the folder escapes the share or is not a folder. + OSError: If the folder cannot be read. + """ + target, rel = _ftp_resolve(root, rel_path) + if not os.path.isdir(target): + raise ValueError(f"not a folder: {rel or '/'}") + entries = [] + for name in sorted(os.listdir(target), key=str.lower): + full = os.path.join(target, name) + try: + is_dir = os.path.isdir(full) + stat = os.stat(full) + except OSError: + continue + entries.append( + { + "name": name, + "dir": is_dir, + "size": 0 if is_dir else stat.st_size, + "mtime": int(stat.st_mtime), + } + ) + entries.sort(key=lambda entry: (not entry["dir"], entry["name"].lower())) + parent = None if rel == "" else (os.path.dirname(rel) or "") + return {"path": rel, "parent": parent, "entries": entries} + +def _load_or_create_secret_key(path=None): + """Persist the Flask session key so logins survive a restart.""" + path = path or SECRET_KEY_FILE + try: + with open(path, "r", encoding="utf-8") as f: + key = f.read().strip() + if key: + return key + except FileNotFoundError: + pass + except OSError: + traceback.print_exc() + key = secrets.token_hex(32) + try: + with open(path, "w", encoding="utf-8") as f: + f.write(key) + try: + os.chmod(path, 0o600) + except OSError: + pass + except OSError: + traceback.print_exc() + return key + def _public_host(host): """Resolve a wildcard bind address to an address clients can reach.""" @@ -130,6 +279,25 @@ def _load_json_list(path): return [] +def _read_config_file(): + """Return the saved startup config, or ``{}`` when it is absent or unreadable.""" + if not os.path.exists(SERVER_CONFIG_FILE): + return {} + try: + with open(SERVER_CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + return {} + return data if isinstance(data, dict) else {} + + +def _write_config_file(config): + """Persist the startup config as indented JSON.""" + os.makedirs(FLOW_WEB_DIR, exist_ok=True) + with open(SERVER_CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(config, f, indent=4, ensure_ascii=False) + + def _config_display_value(key, value): """Render a saved config value for the config form input.""" if value is None: @@ -142,7 +310,17 @@ def _config_display_value(key, value): class ServerWebApp: """Flask app + TCP_Server_Base wrapper for the web tool.""" - def __init__(self, web_port=None): + def __init__(self, web_port=None, db_path=None, mail_config_path=None): + """Create the Flask app and its stores. + + Args: + web_port (int | None): Port of this web backend; defaults to + ``DEFAULT_WEB_PORT``. A busy port falls back to the next free one. + db_path (str | None): SQLite account database; defaults to + ``.Flow_Web/flow_web.db``. + mail_config_path (str | None): JSON file with the SMTP settings; + defaults to ``.Flow_Web/email_config.json``. + """ self.web_port = web_port or DEFAULT_WEB_PORT self.server = None self.mode = "config" # "config" | "status" @@ -153,36 +331,51 @@ def __init__(self, web_port=None): self._events = [] # inbound events surfaced to the frontend (/api/events) self._events_lock = threading.Lock() self._event_seq = 0 + self._addr_tokens = {} # connected client address -> account session token + self._bind_lock = threading.Lock() + self.ftp_root = None # shared folder of the web "ftp" server (host path) + self._ftp_lock = threading.Lock() + + self.users = UserDatabase(db_path) + self.mail = MailService(mail_config_path) self.app = Flask( __name__, template_folder=TEMPLATE_DIR, static_folder=STATIC_DIR, static_url_path="/static", ) + self.app.secret_key = _load_or_create_secret_key() + self.app.config.update( + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Lax", + ) + self._register_routes() # ------------------------------------------------------------------ setup def start_from_config(self): - """Read ``.Flow_Web/setup_server.json`` and start the TCP server.""" - if not os.path.exists(SERVER_CONFIG_FILE): - self.mode = "config" - return - with open(SERVER_CONFIG_FILE, "r", encoding="utf-8") as f: - data = json.load(f) + """Read ``.Flow_Web/setup_server.json`` and start the TCP server. + + The saved "ftp" share is restored as well, so a folder shared before the + restart keeps being served. + """ + data = _read_config_file() servers = data.get("servers", []) if not servers: self.mode = "config" return web = data.get("web", {}) or {} self.web_port = int(web.get("port", DEFAULT_WEB_PORT)) + ftp_root = web.get("ftp_root") + if isinstance(ftp_root, str) and os.path.isdir(ftp_root): + with self._ftp_lock: + self.ftp_root = ftp_root self._start_server(servers[0]) def _start_server(self, config): """Create, register and start the TCP_Server_Base instance.""" params = dict(config) - # Web architecture constraints: extensions must be registered - # before start, and the web UI replaces the console input. params["is_extend_command"] = True params["is_input_command_in_console"] = False if params.get("is_custom_keys") in (None, ""): @@ -198,6 +391,10 @@ def _start_server(self, config): self.server.register_command( "/web_sync_clients", self._on_sync_clients, where_to_run="server", run_in_thread=True ) + self.server.register_command( + BIND_COMMAND, self._on_web_bind, where_to_run="server", run_in_thread=True + ) + self._register_ftp_commands() self.server.add_message_listener(self._on_incoming_message) self.server.add_file_listener(self._on_incoming_file) try: @@ -223,6 +420,9 @@ def _monitor_loop(self): continue with self.server.client_lock: current = set(self.server.clients.keys()) + with self._bind_lock: + for addr in [a for a in self._addr_tokens if a not in current]: + del self._addr_tokens[addr] if current != self._last_clients: self._last_clients = current self._broadcast_clients() @@ -230,7 +430,13 @@ def _monitor_loop(self): last_check = time.time() self._broadcast_clients() - def _client_list(self): + def _connected_entries(self): + """Return one entry per connected TCP client, as the sidebar shows them. + + Returns: + list[dict]: ``{"ip", "port", "id"}`` per connection, in the order the + server holds them. + """ if self.server is None: return [] with self.server.client_lock: @@ -240,30 +446,243 @@ def _client_list(self): ] def _server_info_payload(self): + """Return the TCP address/port web clients connect to. + + Returns: + dict: ``{"host", "port", "is_enable_encrypto"}`` of the running TCP + server; ``host`` is resolved when it binds a wildcard address. + """ return { "host": _public_host(self.server.host), "port": self.server.port, "is_enable_encrypto": self.server.is_enable_encrypto, } + def _account_of(self, addr): + """Resolve the account bound to a connected client address. + + Args: + addr (tuple): Peer ``(ip, port)`` of the TCP connection. + + Returns: + dict | None: Account without its password, or ``None`` while the + connection has not bound a valid session token. + """ + with self._bind_lock: + token = self._addr_tokens.get(tuple(addr)) + return self.users.session_user(token) if token else None + + def _bound_addresses(self): + """Map every bound account to the address its client connects from. + + Returns: + dict: ``user_id`` -> ``{"ip", "port"}`` for the accounts with a live + bound connection. + """ + with self._bind_lock: + bound = dict(self._addr_tokens) + mapping = {} + for addr, token in bound.items(): + user = self.users.session_user(token) + if user is not None: + mapping.setdefault(user["user_id"], {"ip": addr[0], "port": addr[1]}) + return mapping + + def _client_list(self): + """List every connected instance with the account bound to it. + + This is the operator view used by the server console. + + Returns: + list[dict]: One entry per connected client; bound entries also carry + ``user_id``, ``username`` and ``email``. + """ + entries = [] + for entry in self._connected_entries(): + account = self._account_of((entry["ip"], entry["port"])) + if account is not None: + entry.update(account) + entries.append(entry) + return entries + + def _client_list_for(self, addr): + """List the connected instances one client is allowed to see. + + A client sees itself nowhere and sees another account only once the two + are contacts; an unbound connection sees no other instance at all. + + Args: + addr (tuple): Peer ``(ip, port)`` of the requesting connection. + + Returns: + list[dict]: Contact entries, each with ``ip``, ``port``, ``id``, + ``user_id``, ``username`` and ``email``. + """ + user = self._account_of(addr) + if user is None: + return [] + contacts = {c["user_id"]: c for c in self.users.contacts(user["user_id"])} + entries = [] + for entry in self._connected_entries(): + account = self._account_of((entry["ip"], entry["port"])) + if account is None or account["user_id"] not in contacts: + continue + entry.update(account) + entries.append(entry) + return entries + def _broadcast_clients(self): - """Push the current instance list to every connected client.""" + """Push every connected client the instance list it may see.""" if self.server is None or not self.server.running: return - payload = json.dumps(self._client_list(), separators=(",", ":")) - message = f"/web_clients_update {payload}" with self.server.client_lock: - for info in list(self.server.clients.values()): - try: - self.server.send_message(info["socket"], message) - except Exception: - pass + targets = list(self.server.clients.items()) + for addr, info in targets: + payload = json.dumps(self._client_list_for(addr), separators=(",", ":")) + try: + self.server.send_message(info["socket"], f"/web_clients_update {payload}") + except Exception: + pass def _on_sync_clients(self, sock, addr, cmd): """A client asked for a fresh instance list: broadcast it.""" self._broadcast_clients() return None + def _on_web_bind(self, sock, addr, cmd): + """Bind a client connection to the account owning the session token. + + Server side of ``/web_bind ``: the client sends the token it got + from ``/api/client_login`` right after connecting, which is what lets the + server filter the instance list by contacts. The client is told which + address the server sees for it, so it can recognise its own entry. + """ + token = cmd[len(BIND_COMMAND) :].strip() + user = self.users.session_user(token) + if user is None: + return None + with self._bind_lock: + self._addr_tokens[tuple(addr)] = token + try: + self.server.send_message( + sock, f"{BIND_OK_COMMAND} {json.dumps({'ip': addr[0], 'port': addr[1]})}" + ) + except Exception: + traceback.print_exc() + self._broadcast_clients() + return None + + # ------------------------------------------------------------- "ftp" share + + def _register_ftp_commands(self): + """Register the web "ftp" share commands on the running TCP server.""" + if self.server is None: + return + self.server.register_command( + FTP_LIST_COMMAND, self._on_ftp_list, where_to_run="server", run_in_thread=True + ) + self.server.register_command( + FTP_GET_COMMAND, self._on_ftp_get, where_to_run="server", run_in_thread=True + ) + + def _ftp_shared_root(self): + """Return the shared folder, or None while nothing is shared.""" + with self._ftp_lock: + return self.ftp_root + + def _persist_ftp_root(self, root): + """Remember the shared folder in the startup config so a restart restores it. + + Args: + root (str | None): Folder now shared, or ``None`` once the share is + taken away. + """ + config = _read_config_file() + web = dict(config.get("web") or {}) + if root: + web["ftp_root"] = root + else: + web.pop("ftp_root", None) + config["servers"] = config.get("servers") or [] + config.setdefault("clients", []) + config["web"] = web + try: + _write_config_file(config) + except OSError: + traceback.print_exc() + + def _on_ftp_list(self, sock, addr, cmd): + """Server side of ``/ftp_list``: answer with the folder listing. + + Not FTP: the answer is a protocol line (``/ftp_list_ok`` or + ``/ftp_error``) carrying a JSON listing of one folder inside the + shared root. + """ + parts = cmd.split(" ", 2) + request_id = parts[1] if len(parts) > 1 else "?" + raw = parts[2].strip() if len(parts) > 2 else "" + try: # the web client sends the path as a JSON string; a raw path also works + decoded = json.loads(raw) + except ValueError: + decoded = None + rel_path = decoded if isinstance(decoded, str) else raw + root = self._ftp_shared_root() + if not root: + return f"{FTP_ERROR_COMMAND} {request_id} no folder is shared" + try: + listing = _ftp_listing(root, rel_path) + except (ValueError, OSError) as e: + return f"{FTP_ERROR_COMMAND} {request_id} {e}" + return f"{FTP_LIST_OK_COMMAND} {request_id} {json.dumps(listing)}" + + def _on_ftp_get(self, sock, addr, cmd): + """Server side of ``/ftp_get``: push the selected share entries. + + Every entry is handed to the protocol's native transfer (``/file`` for + a file, ``/file_folder`` for a folder) addressed to the asking client, + carrying the receiver-side destination the client asked for (the + receiver's default transfer folder when it asked for none). + + The request body is the list of share-relative entries; a client may + also send ``{"paths": [...], "destination": "..."}`` to pick where the + entries land on its own host. + """ + parts = cmd.split(" ", 2) + request_id = parts[1] if len(parts) > 1 else "?" + try: + request = json.loads(parts[2]) if len(parts) > 2 else [] + except ValueError: + return f"{FTP_ERROR_COMMAND} {request_id} malformed request" + destination = None + if isinstance(request, dict): + raw_destination = request.get("destination") + destination = str(raw_destination).strip() if raw_destination else None + request = request.get("paths") + if not isinstance(request, list): + return f"{FTP_ERROR_COMMAND} {request_id} malformed request" + root = self._ftp_shared_root() + if not root: + return f"{FTP_ERROR_COMMAND} {request_id} no folder is shared" + if self._target_info(addr) is None: + return f"{FTP_ERROR_COMMAND} {request_id} client is not connected" + started = 0 + skipped = 0 + for entry in request: + try: + target, _rel = _ftp_resolve(root, str(entry)) + except ValueError: + skipped += 1 + continue + if not os.path.exists(target): + skipped += 1 + continue + if os.path.isdir(target): + self._send_folder_to_client(tuple(addr), target, destination) + else: + self._send_file_to_client(tuple(addr), target, destination) + started += 1 + return f"{FTP_GET_OK_COMMAND} {request_id} {started} {skipped}" + # ------------------------------------------------ inbound event handling def _push_event(self, event): @@ -319,6 +738,160 @@ def _require_server(self): return jsonify({"ok": False, "error": "TCP server is not running"}), 503 return None + # ------------------------------------------------------------------- auth + + def _current_user(self): + """Session user re-resolved against the store, so removed users lose access.""" + username = session.get("username") + if not username: + return None + user = self.users.find(username) + if user is None: + session.clear() + return None + return user + + def _session_user(self): + """Resolve the client session token carried by a request. + + Returns: + dict | None: Account bound to the token, or ``None`` for a missing + or unknown token. + """ + token = request.args.get("token", "") + data = request.get_json(silent=True) + if isinstance(data, dict) and data.get("token"): + token = str(data["token"]) + return self.users.session_user(token) + + def _send_code(self, purpose, target): + """Issue a verification code for an email address and deliver it. + + Args: + purpose (str): "register", "login" or "reset_password". + target (str): Recipient email address. + + Returns: + dict: ``{"expires_in", "resend_after", "masked_email"}`` describing + the delivered code. + + Raises: + ValueError: If a code was requested for that purpose and address too + recently, or the mailbox is not configured or refuses the + message. A code that could not be delivered is dropped. + """ + issued = self.users.issue_code(purpose, target) + try: + self.mail.send_code(target, issued["code"], purpose, issued["expires_in"]) + except ValueError: + self.users.discard_codes(purpose, target) + raise + return { + "expires_in": issued["expires_in"], + "resend_after": issued["resend_after"], + "masked_email": mask_email(target), + } + + def _account_with_email(self, identify): + """Return the account matching ``identify`` together with its email. + + Args: + identify (str): User id, username or email address. + + Returns: + tuple: ``(account, email)`` for the matching account. + + Raises: + ValueError: If no account matches, or it has no email address. + """ + account = self.users.find(identify) + if account is None: + raise ValueError("no account matches that user name or email") + if not account["email"]: + raise ValueError("this account has no email address; ask an administrator") + return account, account["email"] + + def _require_login(self): + if self._current_user() is None: + return jsonify({"ok": False, "error": "login required"}), 401 + return None + + def _require_admin(self): + user = self._current_user() + if user is None: + return jsonify({"ok": False, "error": "login required"}), 401 + if user["role"] != "admin": + return jsonify({"ok": False, "error": "administrator privileges required"}), 403 + return None + + def _default_admin_credentials_in_use(self, username): + """Report whether the seeded administrator still accepts the seeded password. + + Args: + username (str): Account whose session is being opened. + + Returns: + bool: True only while the seeded username still logs in with + ``DEFAULT_ADMIN_PASSWORD``. + """ + return username == DEFAULT_ADMIN_USERNAME and ( + self.users.authenticate(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD) is not None + ) + + def _page_context(self, user): + """Render the account details every console page shows.""" + return { + "role": user["role"], + "username": user["username"], + "user_id": user["user_id"], + "email": user["email"] or "", + "must_change_credentials": bool(session.get("must_change_credentials")), + } + + def _landing_context(self): + """Addresses shown on the landing page (``None`` while the server is down).""" + if self.server is None or not self.server.running: + return None + host = _public_host(self.server.host) + return { + "web": f"http://{host}:{self._bound_port or self.web_port}/", + "tcp": f"{host}:{self.server.port}", + } + + def _config_form_fields(self): + """``(server, web)`` form rows for the startup-configuration page.""" + current = {} + web_port = self.web_port + if os.path.exists(SERVER_CONFIG_FILE): + try: + with open(SERVER_CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + servers = data.get("servers", []) + if servers: + current = servers[0] + web = data.get("web", {}) or {} + web_port = int(web.get("port", web_port)) + except Exception: + pass + fields = [ + (key, label, ftype, _config_display_value(key, current.get(key, default)), help) + for key, label, ftype, default, help in SERVER_PARAM_FIELDS + ] + web_fields = [ + (key, label, ftype, web_port, help) for key, label, ftype, default, help in WEB_FIELDS + ] + return fields, web_fields + + def _render_config(self, user): + """Render the startup-configuration page for an authenticated administrator.""" + fields, web_fields = self._config_form_fields() + return render_template( + "server_config.html", + fields=fields, + web_fields=web_fields, + **self._page_context(user), + ) + def _target_info(self, target): addr = (target[0], int(target[1])) with self.server.client_lock: @@ -385,41 +958,64 @@ def _stop_server(self): def _register_routes(self): app = self.app + def login_required(view): + """Reject requests without a valid session.""" + + @functools.wraps(view) + def wrapped(*args, **kwargs): + err = self._require_login() + if err is not None: + return err + return view(*args, **kwargs) + + return wrapped + + def admin_required(view): + """Reject requests from users that are not administrators.""" + + @functools.wraps(view) + def wrapped(*args, **kwargs): + err = self._require_admin() + if err is not None: + return err + return view(*args, **kwargs) + + return wrapped + + def client_required(view): + """Reject requests without a valid client session token.""" + + @functools.wraps(view) + def wrapped(*args, **kwargs): + user = self._session_user() + if user is None: + return jsonify({"ok": False, "error": "login required"}), 401 + return view(user, *args, **kwargs) + + return wrapped + @app.get("/") def index(): - if self.mode == "status": - return render_template("server_status.html", mode="server") - return render_template( - "server_config.html", fields=SERVER_PARAM_FIELDS, web_fields=WEB_FIELDS - ) + user = self._current_user() + if user is None: + return render_template("server_landing.html", hint=self._landing_context()) + if self.mode == "status" or user["role"] != "admin": + # The startup-configuration page is administrator-only. + return render_template( + "server_status.html", mode="server", **self._page_context(user) + ) + return self._render_config(user) @app.get("/config") def config(): """Startup-configuration page, reachable from the status page too.""" - current = {} - web_port = self.web_port - if os.path.exists(SERVER_CONFIG_FILE): - try: - with open(SERVER_CONFIG_FILE, "r", encoding="utf-8") as f: - data = json.load(f) - servers = data.get("servers", []) - if servers: - current = servers[0] - web = data.get("web", {}) or {} - web_port = int(web.get("port", web_port)) - except Exception: - pass - fields = [ - (key, label, ftype, _config_display_value(key, current.get(key, default)), help) - for key, label, ftype, default, help in SERVER_PARAM_FIELDS - ] - web_fields = [ - (key, label, ftype, web_port, help) - for key, label, ftype, default, help in WEB_FIELDS - ] - return render_template("server_config.html", fields=fields, web_fields=web_fields) + user = self._current_user() + if user is None or user["role"] != "admin": + return redirect("/") + return self._render_config(user) @app.get("/api/status") + @login_required def api_status(): return jsonify( { @@ -432,14 +1028,17 @@ def api_status(): ) @app.post("/api/save_config") + @admin_required def api_save_config(): data = request.get_json(force=True) params = data.get("params", {}) web_port = int(data.get("web_port", DEFAULT_WEB_PORT)) - os.makedirs(FLOW_WEB_DIR, exist_ok=True) - config = {"servers": [params], "clients": [], "web": {"port": web_port}} - with open(SERVER_CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(config, f, indent=4, ensure_ascii=False) + # Keep every web-only setting (the shared "ftp" folder among them) + # that this form does not own. + web = dict(_read_config_file().get("web") or {}) + web["port"] = web_port + config = {"servers": [params], "clients": [], "web": web} + _write_config_file(config) self.web_port = web_port if self.server is not None and web_port == self._bound_port: # Same web port: restart the TCP server in place; the Flask @@ -475,10 +1074,12 @@ def api_server_info(): return jsonify(self._server_info_payload()) @app.get("/api/clients") + @login_required def api_clients(): return jsonify({"clients": self._client_list()}) @app.get("/api/events") + @login_required def api_events(): since = request.args.get("since", 0, type=int) with self._events_lock: @@ -487,6 +1088,7 @@ def api_events(): return jsonify({"events": events, "latest": latest}) @app.post("/api/send_msg") + @login_required def api_send_msg(): err = self._require_server() if err: @@ -506,6 +1108,7 @@ def api_send_msg(): return jsonify({"ok": True}) @app.post("/api/send_file") + @login_required def api_send_file(): err = self._require_server() if err: @@ -535,6 +1138,7 @@ def api_send_file(): return jsonify({"ok": True, "paths": saved}) @app.post("/api/send_folder") + @login_required def api_send_folder(): err = self._require_server() if err: @@ -568,6 +1172,7 @@ def api_send_folder(): return jsonify({"ok": True, "path": root}) @app.post("/api/run_extension") + @admin_required def api_run_extension(): err = self._require_server() if err: @@ -584,6 +1189,7 @@ def api_run_extension(): return jsonify({"ok": True}) @app.get("/api/available_commands") + @admin_required def api_available_commands(): err = self._require_server() if err: @@ -591,19 +1197,67 @@ def api_available_commands(): return jsonify({"commands": sorted(self.server._custom_handlers[1].keys())}) @app.post("/api/sync_clients") + @login_required def api_sync_clients(): self._broadcast_clients() return jsonify({"ok": True}) + @app.get("/api/ftp") + @login_required + def api_ftp_status(): + root = self._ftp_shared_root() + return jsonify( + {"ok": True, "root": root, "shared": bool(root and os.path.isdir(root))} + ) + + @app.post("/api/ftp/add") + @admin_required + def api_ftp_add(): + data = request.get_json(force=True) + raw = (data.get("path") or "").strip() + if not raw: + return jsonify({"ok": False, "error": "a folder path is required"}), 400 + path = os.path.abspath(os.path.expanduser(raw)) + if not os.path.isdir(path): + return jsonify({"ok": False, "error": f"not a folder: {path}"}), 400 + with self._ftp_lock: + self.ftp_root = path + self._persist_ftp_root(path) + self._register_ftp_commands() + return jsonify({"ok": True, "root": path}) + + @app.post("/api/ftp/remove") + @admin_required + def api_ftp_remove(): + with self._ftp_lock: + self.ftp_root = None + self._persist_ftp_root(None) + return jsonify({"ok": True}) + + @app.get("/api/ftp/list") + @login_required + def api_ftp_list(): + root = self._ftp_shared_root() + if not root: + return jsonify({"ok": False, "error": "no folder is shared"}), 404 + try: + listing = _ftp_listing(root, request.args.get("path", "")) + except (ValueError, OSError) as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "root": root, "listing": listing}) + @app.get("/api/extensions_ui") + @admin_required def api_get_extensions_ui(): return jsonify({"extensions": _load_json_list(SERVER_EXTENSIONS_UI_FILE)}) @app.get("/api/registered_extensions") + @admin_required def api_registered_extensions(): return jsonify({"extensions": _load_json_list(add_extension.added_extensions_log_file)}) @app.post("/api/extensions_ui") + @admin_required def api_save_extensions_ui(): data = request.get_json(force=True) entries = data.get("extensions", []) @@ -613,6 +1267,7 @@ def api_save_extensions_ui(): return jsonify({"ok": True}) @app.post("/api/add_extension") + @admin_required def api_add_extension(): data = request.get_json(force=True) paths = data.get("paths", []) @@ -624,6 +1279,7 @@ def api_add_extension(): return jsonify({"ok": True, "restarting": True}) @app.post("/api/remove_extension") + @admin_required def api_remove_extension(): data = request.get_json(force=True) paths = data.get("paths", []) @@ -634,6 +1290,345 @@ def api_remove_extension(): threading.Thread(target=self._restart, daemon=True).start() return jsonify({"ok": True, "restarting": True}) + # ------------------------------------------------------------- accounts + + # Public flows of the landing page: registration, password reset and + # the mailbox that delivers their verification codes. + + @app.post("/api/register/send_code") + def api_register_send_code(): + """Mail a registration code to an address that is still free.""" + data = request.get_json(silent=True) or {} + email = str(data.get("email") or "").strip() + try: + if self.users.email_registered(email): + return jsonify({"ok": False, "error": "this email is already registered"}), 400 + return jsonify({"ok": True, **self._send_code("register", email)}) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + + @app.post("/api/register") + def api_register(): + """Create an account once the mailed code checks out.""" + data = request.get_json(silent=True) or {} + try: + user = self.users.register_with_code( + str(data.get("username") or ""), + str(data.get("email") or ""), + str(data.get("password") or ""), + str(data.get("code") or ""), + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "user": user}) + + @app.post("/api/login/send_code") + def api_login_send_code(): + """Mail a login code to the address of an existing account.""" + data = request.get_json(silent=True) or {} + try: + _, email = self._account_with_email(str(data.get("identify") or "")) + return jsonify({"ok": True, **self._send_code("login", email)}) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + + @app.post("/api/password/send_code") + def api_password_send_code(): + """Mail a password-reset code to the address of an existing account.""" + data = request.get_json(silent=True) or {} + try: + _, email = self._account_with_email(str(data.get("identify") or "")) + return jsonify({"ok": True, **self._send_code("reset_password", email)}) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + + @app.post("/api/password/reset") + def api_password_reset(): + """Set a new password once the mailed reset code checks out.""" + data = request.get_json(silent=True) or {} + try: + self.users.reset_password_with_code( + str(data.get("identify") or ""), + str(data.get("code") or ""), + str(data.get("password") or ""), + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True}) + + # Client accounts: a web client logs in here and receives the session + # token that binds its TCP connection to the account. + + @app.post("/api/client_login") + def api_client_login(): + """Log a web client in against its password and a mailed verification code.""" + data = request.get_json(silent=True) or {} + identify = str(data.get("identify") or "").strip() + password = str(data.get("password") or "") + code = str(data.get("code") or "").strip() + if not identify: + return jsonify({"ok": False, "error": "enter your user name or email"}), 400 + if not password or not code: + return ( + jsonify( + { + "ok": False, + "error": "enter the account password and the mailed verification code", + } + ), + 400, + ) + user = self.users.authenticate(identify, password) + if user is None: + return jsonify({"ok": False, "error": "invalid account or password"}), 401 + try: + _, email = self._account_with_email(identify) + self.users.verify_code("login", email, code) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 401 + token = self.users.create_session(user["user_id"]) + return jsonify({"ok": True, "token": token, "user": user}) + + @app.post("/api/client_verify") + def api_client_verify(): + """Check a stored client session, optionally against its saved credentials. + + A client that logs itself in again from + ``.Flow_Web/client_login.json`` sends the saved account and password + along with the token; the token alone only proves the session is + known, while the credentials prove they still open that account. + """ + data = request.get_json(silent=True) or {} + user = self._session_user() + if user is None: + return jsonify({"ok": False, "error": "login required"}), 401 + identify = str(data.get("identify") or "").strip() + password = str(data.get("password") or "") + if bool(identify) != bool(password): + return ( + jsonify( + {"ok": False, "error": "send the saved account and its password together"} + ), + 400, + ) + if identify: + account = self.users.authenticate(identify, password) + if account is None or account["user_id"] != user["user_id"]: + return ( + jsonify( + { + "ok": False, + "error": "the saved credentials no longer open this account", + } + ), + 401, + ) + return jsonify({"ok": True, "user": user}) + + @app.post("/api/client_logout") + def api_client_logout(): + """Invalidate a client session token.""" + data = request.get_json(silent=True) or {} + self.users.drop_session(str(data.get("token") or "")) + self._broadcast_clients() + return jsonify({"ok": True}) + + # Contacts: a client only ever sees the accounts it is a contact of. + + @app.post("/api/contacts/search") + @client_required + def api_search_contacts(user): + """Search accounts by user id, username or email.""" + data = request.get_json(silent=True) or {} + try: + matches = self.users.search_users( + str(data.get("query") or ""), user["user_id"] + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + contacts = {c["user_id"] for c in self.users.contacts(user["user_id"])} + requests = self.users.contact_requests(user["user_id"]) + outgoing = {r["user"]["user_id"] for r in requests["outgoing"]} + incoming = {r["user"]["user_id"] for r in requests["incoming"]} + online = self._bound_addresses() + results = [] + for match in matches: + entry = dict(match) + if match["user_id"] in contacts: + entry["relation"] = "contact" + elif match["user_id"] in outgoing: + entry["relation"] = "outgoing" + elif match["user_id"] in incoming: + entry["relation"] = "incoming" + else: + entry["relation"] = "none" + entry["online"] = match["user_id"] in online + results.append(entry) + return jsonify({"ok": True, "results": results}) + + @app.post("/api/contacts/request") + @client_required + def api_request_contact(user): + """Ask another account to become a contact.""" + data = request.get_json(silent=True) or {} + try: + target = self.users.request_contact( + user["user_id"], str(data.get("user_id") or "") + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "user": target}) + + @app.get("/api/contact_requests") + @client_required + def api_contact_requests(user): + """List the pending contact requests of the logged-in client.""" + return jsonify({"ok": True, **self.users.contact_requests(user["user_id"])}) + + @app.post("/api/contacts/respond") + @client_required + def api_respond_contact(user): + """Accept or reject one incoming contact request.""" + data = request.get_json(silent=True) or {} + try: + request_id = int(data.get("request_id")) + except (TypeError, ValueError): + return jsonify({"ok": False, "error": "invalid contact request"}), 400 + accept = bool(data.get("accept")) + try: + requester = self.users.respond_request(user["user_id"], request_id, accept) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + self._broadcast_clients() + return jsonify({"ok": True, "accepted": accept, "user": requester}) + + # Verification mailbox (administrators only): the settings are checked + # against the real server before they are stored. + + @app.get("/api/email_config") + @admin_required + def api_get_email_config(): + """Return the stored SMTP settings of the verification mailbox.""" + config = self.mail.get_config() or {} + config.pop("password", None) + return jsonify({"ok": True, "enabled": self.mail.is_enabled(), "config": config}) + + @app.post("/api/email_config") + @admin_required + def api_set_email_config(): + """Validate and store the SMTP settings, starting the mail service.""" + data = request.get_json(silent=True) or {} + try: + config = self.mail.configure(data.get("config") or data) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + config.pop("password", None) + return jsonify({"ok": True, "enabled": True, "config": config}) + + # Server console session and user administration. + + @app.post("/api/login") + def api_login(): + """Open the console session of a server user.""" + data = request.get_json(silent=True) or {} + identify = str(data.get("identify") or "").strip() + password = str(data.get("password") or "") + user = self.users.authenticate(identify, password) + if user is None: + return jsonify({"ok": False, "error": "invalid account or password"}), 401 + session.clear() + session["username"] = user["username"] + session["role"] = user["role"] + session["must_change_credentials"] = self._default_admin_credentials_in_use( + user["username"] + ) + return jsonify( + { + "ok": True, + "username": user["username"], + "role": user["role"], + "must_change_credentials": session["must_change_credentials"], + "redirect": "/", + } + ) + + @app.post("/api/logout") + def api_logout(): + """Close the console session.""" + session.clear() + return jsonify({"ok": True}) + + @app.post("/api/account") + @login_required + def api_account(): + """Change own username, email or password; the current password is required.""" + user = self._current_user() + data = request.get_json(silent=True) or {} + current_password = str(data.get("current_password") or "") + new_username = str(data.get("username") or "").strip() + new_password = str(data.get("password") or "") or None + email = str(data.get("email") or "").strip() or None + if self.users.authenticate(user["username"], current_password) is None: + return jsonify({"ok": False, "error": "current password is incorrect"}), 403 + try: + updated = self.users.update_credentials( + user["user_id"], + new_username=new_username, + new_password=new_password, + email=email, + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + session["username"] = updated["username"] + session["must_change_credentials"] = self._default_admin_credentials_in_use( + updated["username"] + ) + return jsonify( + { + "ok": True, + "username": updated["username"], + "user": updated, + "must_change_credentials": session["must_change_credentials"], + } + ) + + @app.get("/api/users") + @admin_required + def api_users(): + """List every account of this server.""" + return jsonify({"users": self.users.list_users()}) + + @app.post("/api/users") + @admin_required + def api_add_user(): + """Create an account without the email registration flow.""" + data = request.get_json(silent=True) or {} + try: + self.users.add_user( + str(data.get("username") or "").strip(), + str(data.get("email") or "").strip(), + str(data.get("password") or ""), + data.get("role"), + ) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "users": self.users.list_users()}) + + @app.post("/api/users/delete") + @admin_required + def api_delete_user(): + """Delete one account, keeping the last administrator.""" + user = self._current_user() + data = request.get_json(silent=True) or {} + username = str(data.get("username") or "").strip() + if username == user["username"]: + return jsonify({"ok": False, "error": "you cannot remove your own account"}), 400 + try: + self.users.remove_user(username) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "users": self.users.list_users()}) + # ------------------------------------------------------------------- run def run(self): diff --git a/PyFlow/transfer_web/web_backend/templates/server_config.html b/PyFlow/transfer_web/web_backend/templates/server_config.html index 2798e11..bde3b75 100644 --- a/PyFlow/transfer_web/web_backend/templates/server_config.html +++ b/PyFlow/transfer_web/web_backend/templates/server_config.html @@ -5,42 +5,95 @@ PyFlow TCP Server Setup +
-
-

PyFlow TCP Server Setup

-

Configure the TCP server. Every parameter has a default value; change only what you need.

-
- {% for key, label, ftype, default, help in fields %} -
- - {% if ftype == "bool" %} -
- - {{ help }} -
- {% else %} - -
{{ help }}
- {% endif %} -
- {% endfor %} - {% for key, label, ftype, default, help in web_fields %} -
- - -
{{ help }}
-
- {% endfor %} -
- - +
+
+
+ {{ username }} · {{ role }} · ID {{ user_id }} +
- +

PyFlow TCP Server Setup

+

Configure the TCP server. Every parameter has a default value; change only what you need.

+
+ {% for key, label, ftype, default, help in fields %} +
+ + {% if ftype == "bool" %} +
+ + {{ help }} +
+ {% else %} + +
{{ help }}
+ {% endif %} +
+ {% endfor %} + {% for key, label, ftype, default, help in web_fields %} +
+ + +
{{ help }}
+
+ {% endfor %} +
+ + +
+
+
+ +
+

Verification email (SMTP)

+

Account verification codes (registration, password reset, code login) are sent + through this mailbox. The settings are checked against the real SMTP server before they are stored + in .Flow_Web/email_config.json; until then no code can be sent. Re-enter the + authorization code when saving.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + diff --git a/PyFlow/transfer_web/web_backend/templates/server_landing.html b/PyFlow/transfer_web/web_backend/templates/server_landing.html new file mode 100644 index 0000000..3375d91 --- /dev/null +++ b/PyFlow/transfer_web/web_backend/templates/server_landing.html @@ -0,0 +1,453 @@ + + + + + + PyFlow Server + + + +
+

The PyFlow Server is running! Connect it in clients by the server host.

+

+ {% if hint %} + Web address: {{ hint.web }}
+ TCP address: {{ hint.tcp }} + {% else %} + The TCP server is not running yet. Log in as the administrator to configure it. + {% endif %} +

+
+ + + +
+
+ + + + + + diff --git a/PyFlow/transfer_web/web_backend/templates/server_status.html b/PyFlow/transfer_web/web_backend/templates/server_status.html index 08b3491..25bf6b7 100644 --- a/PyFlow/transfer_web/web_backend/templates/server_status.html +++ b/PyFlow/transfer_web/web_backend/templates/server_status.html @@ -14,12 +14,17 @@

PyFlow TCP Server

connecting...
- +
@@ -38,13 +43,28 @@

PyFlow TCP Server

+ {% if role == "admin" %} + {% endif %}
- + + + diff --git a/PyFlow/transfer_web/web_backend/user_database.py b/PyFlow/transfer_web/web_backend/user_database.py new file mode 100644 index 0000000..6a2a9a6 --- /dev/null +++ b/PyFlow/transfer_web/web_backend/user_database.py @@ -0,0 +1,1021 @@ +"""SQLite account, verification-code, contact and client-session store. + +A PyFlow web server keeps every account in ``.Flow_Web/flow_web.db``: the +administrator console logins and the client accounts registered from the +server's landing page. Each account carries a public user id next to its +username and email, and is looked up by any of the three. + +A missing database file is created and seeded with the default administrator +(``DEFAULT_ADMIN_USERNAME`` / ``DEFAULT_ADMIN_PASSWORD``); a legacy +``.Flow_Web/users.json`` is imported once and renamed. A database file that +exists but cannot be read is never re-seeded, so a damaged store cannot silently +restore the default account. +""" + +import functools +import hashlib +import hmac +import json +import os +import re +import secrets +import sqlite3 +import threading +import time +import traceback + +WEB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FLOW_WEB_DIR = os.path.join(WEB_ROOT, ".Flow_Web") +USER_DB_FILE = os.path.join(FLOW_WEB_DIR, "flow_web.db") +LEGACY_USERS_FILE = os.path.join(FLOW_WEB_DIR, "users.json") + +# The first run seeds this administrator, and the console warns while the seeded +# pair (and only that pair) is still in use. +DEFAULT_ADMIN_USERNAME = "admin" +DEFAULT_ADMIN_PASSWORD = "admin" + +MIN_PASSWORD_LENGTH = 8 +PBKDF2_ITERATIONS = 200000 +USER_ID_BYTES = 4 # 8 hexadecimal characters +SESSION_TOKEN_BYTES = 32 + +# Verification codes: one lifetime for every purpose, one per minute and target. +CODE_PURPOSES = ("register", "login", "reset_password") +CODE_DIGITS = 6 +CODE_TTL_SECONDS = 300 +CODE_RESEND_SECONDS = 60 +CODE_MAX_ATTEMPTS = 5 + +SEARCH_RESULT_LIMIT = 20 + +_USERNAME_RE = re.compile(r"\S{1,64}") +_EMAIL_RE = re.compile(r"[^@\s]{1,64}@[^@\s]{1,255}\.[A-Za-z]{2,}") + +_SCHEMA = ( + """ + CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY COLLATE NOCASE, + username TEXT NOT NULL UNIQUE COLLATE NOCASE, + email TEXT UNIQUE COLLATE NOCASE, + password TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + created_at REAL NOT NULL + ) + """, + """ + CREATE TABLE IF NOT EXISTS verification_codes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + purpose TEXT NOT NULL, + target TEXT NOT NULL, + code_hash TEXT NOT NULL, + created_at REAL NOT NULL, + expires_at REAL NOT NULL, + used INTEGER NOT NULL DEFAULT 0, + attempts INTEGER NOT NULL DEFAULT 0 + ) + """, + """ + CREATE TABLE IF NOT EXISTS contacts ( + owner_id TEXT NOT NULL COLLATE NOCASE, + contact_id TEXT NOT NULL COLLATE NOCASE, + created_at REAL NOT NULL, + PRIMARY KEY (owner_id, contact_id) + ) + """, + """ + CREATE TABLE IF NOT EXISTS contact_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_id TEXT NOT NULL COLLATE NOCASE, + to_id TEXT NOT NULL COLLATE NOCASE, + status TEXT NOT NULL DEFAULT 'pending', + created_at REAL NOT NULL, + UNIQUE (from_id, to_id) + ) + """, + """ + CREATE TABLE IF NOT EXISTS client_tokens ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL COLLATE NOCASE, + created_at REAL NOT NULL + ) + """, +) + +SEARCH_RESULT_LIMIT = 20 +SEARCH_QUERY_MAX_LENGTH = 64 + + +def hash_password(password, salt, iterations=PBKDF2_ITERATIONS): + """Build a PBKDF2-SHA256 password record. + + Args: + password (str): Plain password to hash. + salt (str): Per-account salt, stored in the record. + iterations (int): PBKDF2 iteration count. Defaults to ``PBKDF2_ITERATIONS``. + + Returns: + str: Record shaped ``pbkdf2_sha256$$$``. + """ + digest = hashlib.pbkdf2_hmac( + "sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations + ) + return f"pbkdf2_sha256${iterations}${salt}${digest.hex()}" + + +def new_password_record(password): + """Build a password record with a fresh random salt. + + Args: + password (str): Plain password to hash. + + Returns: + str: Record produced by `hash_password` with a new 16-byte hex salt. + """ + return hash_password(password, secrets.token_hex(16)) + + +@functools.lru_cache(maxsize=1) +def dummy_password_record(): + """Return a throwaway record, so an unknown account costs a real verification.""" + return new_password_record(secrets.token_hex(32)) + + +def verify_password(password, record): + """Check a plain password against a stored record. + + Args: + password (str): Plain password to check. + record (str): Record produced by `hash_password`. + + Returns: + bool: True when the password matches the record; False for a malformed + or mismatching record. + """ + try: + algorithm, iterations, salt, digest = record.split("$") + if algorithm != "pbkdf2_sha256": + return False + expected = hashlib.pbkdf2_hmac( + "sha256", password.encode("utf-8"), salt.encode("utf-8"), int(iterations) + ) + except Exception: + return False + return hmac.compare_digest(expected.hex(), digest) + + +def validate_username(username): + """Validate an account username. + + Args: + username (str): Login name to validate. + + Returns: + str: The username with surrounding whitespace removed. + + Raises: + ValueError: If the username is not 1-64 characters without whitespace. + """ + username = (username or "").strip() + if not _USERNAME_RE.fullmatch(username): + raise ValueError("the username must be 1-64 characters without spaces") + return username + + +def validate_email(email): + """Validate an email address. + + Args: + email (str): Address to validate. + + Returns: + str: The address with surrounding whitespace removed. + + Raises: + ValueError: If the address is not ``local@domain.tld`` shaped. + """ + email = (email or "").strip() + if not _EMAIL_RE.fullmatch(email): + raise ValueError("enter a valid email address") + return email + + +def validate_password(password): + """Validate a plain password. + + Args: + password (str): Password to validate. + + Returns: + str: The password unchanged. + + Raises: + ValueError: If the password is shorter than ``MIN_PASSWORD_LENGTH``. + """ + password = password or "" + if len(password) < MIN_PASSWORD_LENGTH: + raise ValueError(f"the password must be at least {MIN_PASSWORD_LENGTH} characters") + return password + + +def mask_email(email): + """Hide the local part of an address for display. + + Args: + email (str | None): Address to mask. + + Returns: + str: ``a***@example.com`` shaped text, or an empty string when ``email`` + is empty. + """ + email = (email or "").strip() + if "@" not in email: + return "" + local, _, domain = email.partition("@") + return f"{local[:1]}***@{domain}" + + +def _public(entry): + """Convert a stored row into the account dictionary handed to callers.""" + return { + "user_id": entry["user_id"], + "username": entry["username"], + "email": entry["email"], + "role": entry["role"], + } + + +def _code_hash(purpose, target, code): + """Hash a verification code together with its purpose and target.""" + payload = f"{purpose}\n{target}\n{code}".encode("utf-8") + return hashlib.sha256(payload).hexdigest() + + +def _like_pattern(query): + """Escape a search query for a case-insensitive LIKE substring match.""" + escaped = query.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return f"%{escaped}%" + + +class UserDatabase: + """SQLite account store with verification codes, contacts and client sessions. + + Every method is safe to call from several threads; writes are serialized on + one connection. + """ + + def __init__(self, path=None): + """Open or create the account database. + + Args: + path (str | None): Database file to use; defaults to + ``.Flow_Web/flow_web.db``. The parent directory is created on + demand, and a missing file is seeded with the default + administrator. + """ + self.path = path or USER_DB_FILE + directory = os.path.dirname(self.path) + self.legacy_users_file = ( + os.path.join(directory, "users.json") if directory else LEGACY_USERS_FILE + ) + self._lock = threading.RLock() + self._conn = None + self._error = None + if directory: + os.makedirs(directory, exist_ok=True) + try: + self._conn = sqlite3.connect(self.path, check_same_thread=False) + self._conn.row_factory = sqlite3.Row + with self._lock: + for statement in _SCHEMA: + self._conn.execute(statement) + self._conn.commit() + self._import_legacy_users() + if self._count("users") == 0: + self._seed_default_admin() + except sqlite3.Error as e: + traceback.print_exc() + self._close_connection() + self._error = f"the account database {self.path} cannot be read: {e}" + try: + os.chmod(self.path, 0o600) + except OSError: + pass + + # ------------------------------------------------------------- internals + + def _require_db(self): + """Raise when the database file could not be opened.""" + if self._conn is None: + raise ValueError(self._error or f"the account database {self.path} is unavailable") + + def _execute(self, sql, params=()): + """Run one writing statement and commit it.""" + self._require_db() + with self._lock: + cursor = self._conn.execute(sql, params) + self._conn.commit() + return cursor + + def _fetch(self, sql, params=()): + """Run one reading statement and return the first row or ``None``.""" + self._require_db() + with self._lock: + return self._conn.execute(sql, params).fetchone() + + def _fetchall(self, sql, params=()): + """Run one reading statement and return every row.""" + self._require_db() + with self._lock: + return self._conn.execute(sql, params).fetchall() + + def _count(self, table): + """Return the number of rows in a table.""" + row = self._fetch(f"SELECT COUNT(*) AS n FROM {table}") # noqa: S608 - table names are internal + return row["n"] if row else 0 + + def _insert_user(self, username, email, password_record, role): + """Insert one account row and return its public dictionary.""" + validate_username(username) + if email is not None: + validate_email(email) + if self._fetch("SELECT 1 FROM users WHERE username = ?", (username,)) is not None: + raise ValueError(f"user {username} already exists") + if email is not None and ( + self._fetch("SELECT 1 FROM users WHERE email = ?", (email,)) is not None + ): + raise ValueError(f"the email {email} is already registered") + user_id = self._new_user_id() + try: + self._execute( + "INSERT INTO users (user_id, username, email, password, role, created_at)" + " VALUES (?, ?, ?, ?, ?, ?)", + ( + user_id, + username, + email, + password_record, + role if role in ("admin", "user") else "user", + time.time(), + ), + ) + except sqlite3.IntegrityError as e: + raise ValueError(f"the account cannot be created: {e}") from e + return self.find(user_id) + + def _new_user_id(self): + """Return a public user id no account uses yet.""" + while True: + user_id = secrets.token_hex(USER_ID_BYTES).upper() + if self._fetch("SELECT 1 FROM users WHERE user_id = ?", (user_id,)) is None: + return user_id + + def _find_row(self, identify): + """Return the stored row matching a user id, username or email.""" + identify = (identify or "").strip() + if not identify: + return None + return self._fetch( + "SELECT * FROM users WHERE user_id = ? OR username = ? OR email = ? LIMIT 1", + (identify, identify, identify), + ) + + def _import_legacy_users(self): + """Import a legacy ``users.json`` next to the database and archive it.""" + legacy = self.legacy_users_file + if self._count("users") > 0 or not os.path.exists(legacy): + return + try: + with open(legacy, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + traceback.print_exc() + return + entries = data.get("users") if isinstance(data, dict) else None + imported = 0 + for entry in entries or []: + if not isinstance(entry, dict): + continue + username, password = entry.get("username"), entry.get("password") + if not username or not password: + continue + try: + self._insert_user(username, None, password, entry.get("role")) + imported += 1 + except ValueError: + continue + if imported: + archived = legacy + ".migrated" + try: + if not os.path.exists(archived): + os.replace(legacy, archived) + except OSError: + traceback.print_exc() + print(f"account store: imported {imported} account(s) from users.json") + + def _seed_default_admin(self): + """Create the default administrator in an empty store.""" + self._insert_user( + DEFAULT_ADMIN_USERNAME, + None, + new_password_record(DEFAULT_ADMIN_PASSWORD), + "admin", + ) + + # -------------------------------------------------------------- accounts + + def register(self, username, email, password): + """Create an account from the registration form. + + Args: + username (str): Login name, 1-64 characters without spaces and + unique in the store. + email (str): Address the verification code was sent to; unique in + the store. + password (str): Plain password, at least ``MIN_PASSWORD_LENGTH`` + characters. + + Returns: + dict: New account without its password (``user_id``, ``username``, + ``email``, ``role``). + + Raises: + ValueError: If a field is malformed or the username or email is + already registered. + """ + validate_password(password) + return self._insert_user( + validate_username(username), + validate_email(email), + new_password_record(password), + "user", + ) + + def email_registered(self, email): + """Report whether an email address already belongs to an account. + + Args: + email (str): Address to look up. + + Returns: + bool: True when one account uses the address. + + Raises: + ValueError: If the address is malformed. + """ + email = validate_email(email) + return self._fetch("SELECT 1 FROM users WHERE email = ?", (email,)) is not None + + def register_with_code(self, username, email, password, code): + """Register an account after consuming the code sent to its email. + + The code is only spent once every field is valid, so a rejected + registration does not cost the user a new code. + + Args: + username (str): Login name, 1-64 characters without spaces and + unique in the store. + email (str): Address the verification code was sent to; unique in + the store. + password (str): Plain password, at least ``MIN_PASSWORD_LENGTH`` + characters. + code (str): Code delivered for the ``register`` purpose. + + Returns: + dict: New account without its password. + + Raises: + ValueError: If a field is malformed, the username or email is taken, + or the code is wrong, expired or missing. + """ + username = validate_username(username) + email = validate_email(email) + validate_password(password) + with self._lock: + self.verify_code("register", email, code) + return self.register(username, email, password) + + def reset_password_with_code(self, identify, code, new_password): + """Set a new password after consuming the code sent to the account email. + + Args: + identify (str): User id, username or email address of the account. + code (str): Code delivered for the ``reset_password`` purpose. + new_password (str): Replacement plain password, at least + ``MIN_PASSWORD_LENGTH`` characters. + + Returns: + dict: Updated account without its password. + + Raises: + ValueError: If no account matches ``identify``, it has no email + address, the new password is malformed, or the code is wrong, + expired or missing. + """ + account = self._find_row(identify) + if account is None: + raise ValueError("no account matches that user name or email") + if not account["email"]: + raise ValueError("this account has no email address; ask an administrator") + validate_password(new_password) + with self._lock: + self.verify_code("reset_password", account["email"], code) + return self.update_credentials(account["user_id"], new_password=new_password) + + def add_user(self, username, email, password, role="user"): + """Create an account on behalf of an administrator. + + Args: + username (str): Login name, 1-64 characters without spaces and + unique in the store. + email (str): Unique address of the account. + password (str): Plain password, at least ``MIN_PASSWORD_LENGTH`` + characters. + role (str): "admin" or "user"; anything else is stored as "user". + + Returns: + dict: New account without its password. + + Raises: + ValueError: If a field is malformed or the username or email is + already registered. + """ + validate_password(password) + return self._insert_user( + validate_username(username), validate_email(email), new_password_record(password), role + ) + + def find(self, identify): + """Look an account up by user id, username or email. + + Args: + identify (str): User id, username or email address; matched + case-insensitively. + + Returns: + dict | None: Account without its password, or ``None`` when nothing + matches. + """ + entry = self._find_row(identify) + return _public(entry) if entry is not None else None + + def authenticate(self, identify, password): + """Check a password against one account. + + Args: + identify (str): User id, username or email address. + password (str): Plain password to check. + + Returns: + dict | None: Account without its password when the password is + correct, otherwise ``None``. + """ + entry = self._find_row(identify) + if entry is None: + verify_password(password, dummy_password_record()) + return None + if not verify_password(password, entry["password"]): + return None + return _public(entry) + + def list_users(self): + """List every account sorted by username. + + Returns: + list[dict]: Accounts without their passwords. + """ + rows = self._fetchall("SELECT * FROM users ORDER BY username") + return [_public(row) for row in rows] + + def update_credentials(self, identify, new_username=None, new_password=None, email=None): + """Update the username, password or email of one account. + + Args: + identify (str): User id, username or email address of the account. + new_username (str | None): Replacement login name; ``None`` keeps + the current one. + new_password (str | None): Replacement plain password; ``None`` + keeps the current one. + email (str | None): Replacement address; ``None`` keeps the current + one. + + Returns: + dict: Updated account without its password. + + Raises: + ValueError: If the account is unknown, a replacement is malformed, + or the new username or email is already in use. + """ + entry = self._find_row(identify) + if entry is None: + raise ValueError(f"unknown user {identify}") + fields = {} + if new_username is not None: + new_username = validate_username(new_username) + taken = self._fetch( + "SELECT 1 FROM users WHERE username = ? AND user_id <> ?", + (new_username, entry["user_id"]), + ) + if taken is not None: + raise ValueError(f"user {new_username} already exists") + fields["username"] = new_username + if email is not None: + email = validate_email(email) + taken = self._fetch( + "SELECT 1 FROM users WHERE email = ? AND user_id <> ?", (email, entry["user_id"]) + ) + if taken is not None: + raise ValueError(f"the email {email} is already registered") + fields["email"] = email + if new_password is not None: + fields["password"] = new_password_record(validate_password(new_password)) + if fields: + assignments = ", ".join(f"{name} = ?" for name in fields) + self._execute( + f"UPDATE users SET {assignments} WHERE user_id = ?", # noqa: S608 - column names are internal + (*fields.values(), entry["user_id"]), + ) + return self.find(entry["user_id"]) + + def remove_user(self, identify): + """Delete one account, keeping at least one administrator. + + Args: + identify (str): User id, username or email address. + + Raises: + ValueError: If the account is unknown or it is the last + administrator. + """ + entry = self._find_row(identify) + if entry is None: + raise ValueError(f"unknown user {identify}") + if entry["role"] == "admin": + admins = self._fetch("SELECT COUNT(*) AS n FROM users WHERE role = 'admin'")["n"] + if admins <= 1: + raise ValueError("the last administrator cannot be removed") + self._execute("DELETE FROM users WHERE user_id = ?", (entry["user_id"],)) + self._execute( + "DELETE FROM contacts WHERE owner_id = ? OR contact_id = ?", + (entry["user_id"], entry["user_id"]), + ) + self._execute( + "DELETE FROM contact_requests WHERE from_id = ? OR to_id = ?", + (entry["user_id"], entry["user_id"]), + ) + self._execute("DELETE FROM client_tokens WHERE user_id = ?", (entry["user_id"],)) + + # ---------------------------------------------------- verification codes + + def issue_code(self, purpose, target): + """Store a fresh verification code for a purpose and target. + + Args: + purpose (str): One of ``CODE_PURPOSES``. + target (str): Email address the code is delivered to. + + Returns: + dict: ``{"code", "expires_in", "resend_after"}`` with the plaintext + code, its lifetime in seconds and the cooldown before another + code may be requested. + + Raises: + ValueError: If the purpose or target is invalid, or a code for the + same purpose and target was issued less than + ``CODE_RESEND_SECONDS`` ago. + """ + if purpose not in CODE_PURPOSES: + raise ValueError(f"unknown verification purpose {purpose}") + target = validate_email(target).lower() + with self._lock: + latest = self._fetch( + "SELECT created_at FROM verification_codes WHERE purpose = ? AND target = ?" + " ORDER BY id DESC LIMIT 1", + (purpose, target), + ) + if latest is not None: + elapsed = time.time() - latest["created_at"] + if elapsed < CODE_RESEND_SECONDS: + wait = int(CODE_RESEND_SECONDS - elapsed) + 1 + raise ValueError(f"wait {wait} seconds before requesting another code") + self._execute("DELETE FROM verification_codes WHERE expires_at < ?", (time.time(),)) + code = f"{secrets.randbelow(10 ** CODE_DIGITS):0{CODE_DIGITS}d}" + self._execute( + "INSERT INTO verification_codes" + " (purpose, target, code_hash, created_at, expires_at, used, attempts)" + " VALUES (?, ?, ?, ?, ?, 0, 0)", + ( + purpose, + target, + _code_hash(purpose, target, code), + time.time(), + time.time() + CODE_TTL_SECONDS, + ), + ) + return {"code": code, "expires_in": CODE_TTL_SECONDS, "resend_after": CODE_RESEND_SECONDS} + + def verify_code(self, purpose, target, code): + """Consume the verification code of a purpose and target. + + Args: + purpose (str): One of ``CODE_PURPOSES``. + target (str): Email address the code was sent to. + code (str): Code as typed by the user. + + Raises: + ValueError: If no code was requested for the pair, it expired, the + code is wrong, or ``CODE_MAX_ATTEMPTS`` wrong entries were + already made. + """ + if purpose not in CODE_PURPOSES: + raise ValueError(f"unknown verification purpose {purpose}") + target = validate_email(target).lower() + code = (code or "").strip() + with self._lock: + entry = self._fetch( + "SELECT * FROM verification_codes WHERE purpose = ? AND target = ? AND used = 0" + " ORDER BY id DESC LIMIT 1", + (purpose, target), + ) + if entry is None: + raise ValueError("request a verification code first") + if entry["expires_at"] < time.time(): + raise ValueError("the verification code has expired, request a new one") + if entry["attempts"] >= CODE_MAX_ATTEMPTS: + raise ValueError("too many wrong codes, request a new one") + if not hmac.compare_digest(entry["code_hash"], _code_hash(purpose, target, code)): + self._execute( + "UPDATE verification_codes SET attempts = attempts + 1 WHERE id = ?", + (entry["id"],), + ) + raise ValueError("the verification code is incorrect") + self._execute("UPDATE verification_codes SET used = 1 WHERE id = ?", (entry["id"],)) + + def discard_codes(self, purpose, target): + """Delete every stored code of a purpose and target. + + Used when the code could not be delivered, so the cooldown does not + block an immediate retry. + + Args: + purpose (str): One of ``CODE_PURPOSES``. + target (str): Email address the code was sent to. + """ + target = validate_email(target).lower() + self._execute( + "DELETE FROM verification_codes WHERE purpose = ? AND target = ?", (purpose, target) + ) + + # -------------------------------------------------------------- contacts + + def search_users(self, query, exclude_user_id=None): + """Find accounts by user id, username or email substring. + + Args: + query (str): Text matched case-insensitively inside the user id, + username or email; 1-64 characters after trimming. + exclude_user_id (str | None): Account to leave out of the results, + usually the searcher's own. + + Returns: + list[dict]: At most ``SEARCH_RESULT_LIMIT`` accounts without their + passwords, sorted by username. + + Raises: + ValueError: If the query is empty or longer than 64 characters. + """ + query = (query or "").strip() + if not query: + raise ValueError("enter a user id, username or email to search for") + if len(query) > SEARCH_QUERY_MAX_LENGTH: + raise ValueError("the search text must be at most 64 characters") + pattern = _like_pattern(query) + rows = self._fetchall( + "SELECT * FROM users WHERE (user_id LIKE ? ESCAPE '\\' OR username LIKE ? ESCAPE '\\'" + " OR email LIKE ? ESCAPE '\\') AND user_id <> ? ORDER BY username LIMIT ?", + (pattern, pattern, pattern, exclude_user_id or "", SEARCH_RESULT_LIMIT), + ) + return [_public(row) for row in rows] + + def contacts(self, user_id): + """List the accounts one account is allowed to see. + + Args: + user_id (str): Owner account. + + Returns: + list[dict]: Contact accounts without their passwords, sorted by + username. + """ + rows = self._fetchall( + "SELECT users.* FROM contacts JOIN users ON users.user_id = contacts.contact_id" + " WHERE contacts.owner_id = ? ORDER BY users.username", + (user_id,), + ) + return [_public(row) for row in rows] + + def are_contacts(self, user_id, other_user_id): + """Report whether two accounts are contacts of each other. + + Args: + user_id (str): First account. + other_user_id (str): Second account. + + Returns: + bool: True when ``user_id`` lists ``other_user_id`` as a contact. + """ + row = self._fetch( + "SELECT 1 FROM contacts WHERE owner_id = ? AND contact_id = ?", + (user_id, other_user_id), + ) + return row is not None + + def request_contact(self, from_user_id, to_user_id): + """Ask another account to become a contact. + + Args: + from_user_id (str): Account sending the request. + to_user_id (str): Account the request is addressed to. + + Returns: + dict: Target account without its password. + + Raises: + ValueError: If the target is unknown, it is the sender's own + account, the two are already contacts, or a request is already + pending. + """ + target = self._find_row(to_user_id) + if target is None: + raise ValueError(f"unknown user {to_user_id}") + if target["user_id"] == from_user_id: + raise ValueError("you cannot add your own account") + if self.are_contacts(from_user_id, target["user_id"]): + raise ValueError(f"{target['username']} is already a contact") + with self._lock: + existing = self._fetch( + "SELECT * FROM contact_requests WHERE from_id = ? AND to_id = ?", + (from_user_id, target["user_id"]), + ) + if existing is not None and existing["status"] == "pending": + raise ValueError(f"a request to {target['username']} is already pending") + if existing is not None: + self._execute( + "UPDATE contact_requests SET status = 'pending', created_at = ? WHERE id = ?", + (time.time(), existing["id"]), + ) + else: + self._execute( + "INSERT INTO contact_requests (from_id, to_id, status, created_at)" + " VALUES (?, ?, 'pending', ?)", + (from_user_id, target["user_id"], time.time()), + ) + return _public(target) + + def contact_requests(self, user_id): + """List the pending contact requests of one account. + + Args: + user_id (str): Account whose requests are read. + + Returns: + dict: ``{"incoming": [...], "outgoing": [...]}`` where each entry is + ``{"id", "user", "created_at"}`` and ``user`` is the other + account without its password. + """ + incoming = self._fetchall( + "SELECT contact_requests.id AS request_id," + " contact_requests.created_at AS asked_at, users.*" + " FROM contact_requests JOIN users ON users.user_id = contact_requests.from_id" + " WHERE contact_requests.to_id = ? AND contact_requests.status = 'pending'" + " ORDER BY contact_requests.created_at", + (user_id,), + ) + outgoing = self._fetchall( + "SELECT contact_requests.id AS request_id," + " contact_requests.created_at AS asked_at, users.*" + " FROM contact_requests JOIN users ON users.user_id = contact_requests.to_id" + " WHERE contact_requests.from_id = ? AND contact_requests.status = 'pending'" + " ORDER BY contact_requests.created_at", + (user_id,), + ) + return { + "incoming": [ + {"id": row["request_id"], "user": _public(row), "created_at": row["asked_at"]} + for row in incoming + ], + "outgoing": [ + {"id": row["request_id"], "user": _public(row), "created_at": row["asked_at"]} + for row in outgoing + ], + } + + def respond_request(self, user_id, request_id, accept): + """Accept or reject a pending contact request addressed to one account. + + Accepting records the contact for both accounts. + + Args: + user_id (str): Account answering the request. + request_id (int): Request id as reported by `contact_requests`. + accept (bool): True to accept the request, False to reject it. + + Returns: + dict: Requester account without its password. + + Raises: + ValueError: If the request is unknown, is not addressed to + ``user_id``, or is not pending any more. + """ + entry = self._fetch("SELECT * FROM contact_requests WHERE id = ?", (request_id,)) + if entry is None or entry["to_id"] != user_id: + raise ValueError("unknown contact request") + if entry["status"] != "pending": + raise ValueError("this contact request was already answered") + requester = self._find_row(entry["from_id"]) + if requester is None: + raise ValueError("the requesting account no longer exists") + now = time.time() + with self._lock: + if accept: + self._execute( + "INSERT OR IGNORE INTO contacts (owner_id, contact_id, created_at)" + " VALUES (?, ?, ?)", + (entry["from_id"], entry["to_id"], now), + ) + self._execute( + "INSERT OR IGNORE INTO contacts (owner_id, contact_id, created_at)" + " VALUES (?, ?, ?)", + (entry["to_id"], entry["from_id"], now), + ) + self._execute( + "UPDATE contact_requests SET status = ? WHERE id = ?", + ("accepted" if accept else "rejected", request_id), + ) + return _public(requester) + + # ------------------------------------------------------ client sessions + + def create_session(self, user_id): + """Open a client session for one account. + + Args: + user_id (str): Account the session belongs to. + + Returns: + str: Session token; clients send it back with every request. + + Raises: + ValueError: If the account is unknown. + """ + entry = self._find_row(user_id) + if entry is None: + raise ValueError(f"unknown user {user_id}") + token = secrets.token_urlsafe(SESSION_TOKEN_BYTES) + self._execute( + "INSERT INTO client_tokens (token, user_id, created_at) VALUES (?, ?, ?)", + (token, entry["user_id"], time.time()), + ) + return token + + def session_user(self, token): + """Resolve a client session token. + + Args: + token (str): Token returned by `create_session`. + + Returns: + dict | None: Account without its password, or ``None`` when the + token is unknown. + """ + token = (token or "").strip() + if not token: + return None + row = self._fetch( + "SELECT users.* FROM client_tokens JOIN users ON users.user_id = client_tokens.user_id" + " WHERE client_tokens.token = ?", + (token,), + ) + return _public(row) if row is not None else None + + def drop_session(self, token): + """Invalidate one client session token. + + Args: + token (str): Token returned by `create_session`; an unknown token is + ignored. + """ + self._execute("DELETE FROM client_tokens WHERE token = ?", ((token or "").strip(),)) + + def _close_connection(self): + """Close and forget the database connection, if one is open.""" + with self._lock: + if self._conn is not None: + try: + self._conn.close() + except sqlite3.Error: + traceback.print_exc() + self._conn = None + + def close(self): + """Close the database connection.""" + self._close_connection() diff --git a/PyFlow/transfer_web/web_front/client_backend.py b/PyFlow/transfer_web/web_front/client_backend.py index 7e435f1..dd0c5b4 100644 --- a/PyFlow/transfer_web/web_front/client_backend.py +++ b/PyFlow/transfer_web/web_front/client_backend.py @@ -22,6 +22,18 @@ client's receive threads through ``TCP_Client_Base``'s ``add_message_listener``/``add_file_listener`` APIs, queued here, and polled by the frontend via ``/api/events``. + +Accounts: a connected client logs in with a server account before the +instance list is usable. A forced login needs both factors — the account +password and a verification code mailed to the account address — while a +client that starts again replays the saved credentials and session token +through ``/api/client_verify``. ``/api/login`` opens the session, the +``/api/contacts/*`` and ``/api/contact_requests`` routes proxy the contact +management to the server, and the session token is sent over TCP with +``/web_bind`` so the server can push the contact list of that account. The +credentials are remembered in ``.Flow_Web/client_login.json`` +(owner-readable only) and the login window is shown whenever no session could +be restored. """ import json @@ -33,8 +45,9 @@ import threading import time import traceback +import urllib.error import urllib.request -from urllib.parse import urlparse +from urllib.parse import quote, urlparse from flask import Flask, jsonify, render_template, request @@ -47,6 +60,7 @@ CLIENT_EXTENSIONS_UI_FILE = os.path.join(FLOW_WEB_DIR, "client_extensions_ui.json") CLIENT_LAST_SERVER_FILE = os.path.join(FLOW_WEB_DIR, "client_last_server.json") CLIENT_CONFIG_FILE = os.path.join(FLOW_WEB_DIR, "setup_client.json") +CLIENT_LOGIN_FILE = os.path.join(FLOW_WEB_DIR, "client_login.json") UPLOAD_DIR = os.path.join(FLOW_WEB_DIR, "uploads") TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") STATIC_DIR = os.path.join(WEB_ROOT, "static") @@ -54,6 +68,28 @@ DEFAULT_CLIENT_WEB_PORT = 5001 DEFAULT_SERVER_WEB_PORT = 5000 +# Account binding: the client tells the server which account its TCP +# connection belongs to, and waits for the acknowledgement carrying the +# address the server sees for it. +BIND_COMMAND = "/web_bind" +BIND_OK_COMMAND = "/web_bind_ok" +BIND_RETRY_LIMIT = 10 +BIND_RETRY_INTERVAL = 1.0 + +# Timeout of one request to the server web backend, and the status its routes +# answer with when the session is gone. +REQUEST_TIMEOUT = 10 + +# Web "ftp" share (not FTP): the server answers ``/ftp_list`` with a folder +# listing of the folder it shares and pushes ``/ftp_get`` selections to this +# client over the protocol's native /file and /file_folder transfers. +FTP_LIST_COMMAND = "/ftp_list" +FTP_GET_COMMAND = "/ftp_get" +FTP_LIST_OK_COMMAND = "/ftp_list_ok" +FTP_GET_OK_COMMAND = "/ftp_get_ok" +FTP_ERROR_COMMAND = "/ftp_error" +UNAUTHORIZED = 401 + # Ordered (key, label, type, default, help) for every TCP_Client_Base # parameter shown in the startup-configuration UI. CLIENT_PARAM_FIELDS = [ @@ -89,6 +125,8 @@ ("is_enable_encrypto", "Enable encryption", "bool", True, "RSA-encrypt the TCP channel."), ("is_custom_keys", "Custom keys", "text", "", "Optional [pub_key_path, pvt_key_path] pair."), ("max_mem_buff", "Max memory buffer (MB)", "number", 2048, "In-memory transfer buffer in MB."), + ("is_debug", "Debug log", "bool", False, "Log execution-process lines as well."), + ("is_print_log", "Print log", "bool", True, "Log at all; False silences the instance."), ] @@ -138,6 +176,56 @@ def _config_display_value(key, value): return value +def _last_server_address(): + """Return the address of the last server the client connected to.""" + if not os.path.exists(CLIENT_LAST_SERVER_FILE): + return "" + try: + with open(CLIENT_LAST_SERVER_FILE, "r", encoding="utf-8") as f: + return str(json.load(f).get("address", "")) + except Exception: + return "" + + +def _http_error_message(error): + """Extract the server's error text from a refused HTTP response.""" + try: + body = json.loads(error.read().decode("utf-8")) + message = body.get("error") if isinstance(body, dict) else None + except Exception: + message = None + return str(message) if message else f"HTTP {error.code}" + + +def _channel_ready(client): + """Report whether a command may be written on the client connection. + + An encrypted connection must not carry a plaintext command before the key + exchange flipped it, and the socket is only usable once the client is + running. + """ + if client is None or client.client_socket is None or not client.running: + return False + if not client.is_enable_encrypto: + return True + with client._crypto_lock: + return client.client_socket in client._encrypted_sockets + + +class _ServerRequestError(ValueError): + """Refusal of the server web backend, carrying its HTTP status.""" + + def __init__(self, status, message): + """Record the status code of the refused request. + + Args: + status (int): HTTP status the server answered with. + message (str): Error text reported by the server. + """ + super().__init__(message) + self.status = status + + class ClientWebApp: """Flask app + TCP_Client_Base wrapper for the web tool.""" @@ -154,6 +242,20 @@ def __init__(self, web_port=None): self._event_seq = 0 self._echo_expect = None # plain text last sent to the server (echo suppression) self._echo_expect_at = 0.0 + + # Account session of the connected server: the token/account pair the + # TCP connection is bound to, plus the last login outcome. + self._server_base = "" + self.session = None + self._login_error = "" + self._saved_identify = "" + self._bind_ack = False + self._bind_pending = False + self._bind_lock = threading.Lock() + self._bound_address = None + self._ftp_lock = threading.Lock() + self._ftp_seq = 0 # request ids for the "ftp" listing/download round trips + self._ftp_waiters = {} # request id -> {"event": Event, "reply": tuple | None} self.app = Flask( __name__, template_folder=TEMPLATE_DIR, @@ -165,6 +267,9 @@ def __init__(self, web_port=None): # ---------------------------------------------------------------- helpers def _own_address(self): + """Return this client's address as the server reports it.""" + if self._bound_address is not None: + return dict(self._bound_address) if self.client is None or self.client.client_socket is None: return None try: @@ -223,6 +328,82 @@ def _forward_folder(self, path, addr, destination=None): except Exception: traceback.print_exc() + def _ftp_request(self, command, payload, timeout=REQUEST_TIMEOUT): + """Send one "ftp" command to the server and wait for its answer. + + Args: + command (str): ``/ftp_list`` or ``/ftp_get``. + payload (object): JSON-serializable request argument. + timeout (float): Seconds to wait for the answer. + + Returns: + tuple: ``(reply_command, reply_payload)`` as sent by the server. + + Raises: + _ServerRequestError: If the client is not connected, the write + fails, the server refuses the request or nothing arrives. + """ + if not self.connected or self.client is None or not _channel_ready(self.client): + raise _ServerRequestError(503, "not connected to the server") + with self._ftp_lock: + self._ftp_seq += 1 + request_id = str(self._ftp_seq) + slot = {"event": threading.Event(), "reply": None} + self._ftp_waiters[request_id] = slot + line = f"{command} {request_id} {json.dumps(payload, separators=(',', ':'))}" + try: + self.client.send_message(self.client.client_socket, line) + except Exception as e: + with self._ftp_lock: + self._ftp_waiters.pop(request_id, None) + raise _ServerRequestError(502, f"cannot reach the server: {e}") from e + try: + if not slot["event"].wait(timeout): + raise _ServerRequestError(504, "the server did not answer in time") + finally: + with self._ftp_lock: + self._ftp_waiters.pop(request_id, None) + reply_command, reply_payload = slot["reply"] + if reply_command == FTP_ERROR_COMMAND: + raise _ServerRequestError(502, str(reply_payload)) + return reply_command, reply_payload + + def _ftp_list(self, rel_path): + """Ask the server for one folder of its shared folder. + + Args: + rel_path (str): Folder relative to the share; "" is the share root. + + Returns: + dict: The server's listing payload. + """ + _command, payload = self._ftp_request(FTP_LIST_COMMAND, rel_path) + return payload + + def _ftp_download(self, rel_paths, destination=None): + """Ask the server to push the selected share entries to this client. + + Args: + rel_paths (list): Share-relative files and folders to download. + destination (str | None): Folder on this host the entries are saved + into; ``None`` keeps the receiver's default transfer folder. + + Returns: + dict: ``{"started": int, "skipped": int}``. + """ + payload = {"paths": list(rel_paths), "destination": destination or ""} + _command, payload = self._ftp_request(FTP_GET_COMMAND, payload) + return payload + + def _register_ftp_commands(self): + """Register the replies of the web "ftp" share on the TCP client.""" + if self.client is None: + return + for command in (FTP_LIST_OK_COMMAND, FTP_GET_OK_COMMAND, FTP_ERROR_COMMAND): + self.client.register_command( + command, self._on_ftp_reply, where_to_run="server", run_in_thread=True + ) + def _restart(self): time.sleep(1) # Spawn a fresh process and exit: ``os.execv`` would keep the Flask @@ -252,6 +433,43 @@ def _on_clients_update(self, sock, addr, cmd): self._clients = clients return None + def _on_bind_ok(self, sock, addr, cmd): + """Server ack: record the address the server sees for this client.""" + try: + info = json.loads(cmd[len(BIND_OK_COMMAND) :].strip()) + address = {"ip": str(info["ip"]), "port": int(info["port"])} + except Exception: + return None + address["id"] = f"{address['ip']}:{address['port']}" + self._bound_address = address + self._bind_ack = True + return None + + def _on_ftp_reply(self, sock, addr, cmd): + """Server answer to one "ftp" request: wake the waiting HTTP request.""" + parts = cmd.split(" ", 2) + if len(parts) < 2: + return None + command = parts[0].lower() + request_id = parts[1] + body = parts[2].strip() if len(parts) > 2 else "" + if command == FTP_LIST_OK_COMMAND: + try: + reply = (command, json.loads(body)) + except ValueError: + reply = (FTP_ERROR_COMMAND, "malformed listing") + elif command == FTP_GET_OK_COMMAND: + started, _, skipped = body.partition(" ") + reply = (command, {"started": int(started or 0), "skipped": int(skipped or 0)}) + else: + reply = (FTP_ERROR_COMMAND, body or "the server refused the request") + with self._ftp_lock: + slot = self._ftp_waiters.get(request_id) + if slot is not None: + slot["reply"] = reply + slot["event"].set() + return None + # ------------------------------------------------ inbound event handling def _push_event(self, event): @@ -344,11 +562,19 @@ def _load_client_params(self): return {} def start_from_config(self): - """Read ``.Flow_Web/setup_client.json`` and start the TCP client.""" + """Start the TCP client from ``.Flow_Web/setup_client.json`` and log in.""" params = self._load_client_params() if not params: return + last = _last_server_address() + if last: + try: + self._server_base = _normalize_address(last) + self._last_address = last + except ValueError: + self._server_base = "" self._start_client_from_params(params) + self._auto_login() def _normalize_client_params(self, params): """Normalize form values into TCP_Client_Base constructor arguments.""" @@ -380,13 +606,18 @@ def _start_client_from_params(self, params): "/web_clients_update", self._on_clients_update, where_to_run="server", run_in_thread=True ) self.client.add_message_listener(self._on_incoming_message) + self._register_ftp_commands() self.client.add_file_listener(self._on_incoming_file) try: add_extension.load_registered_extensions(self.client, "client") except ImportError as e: print(f"Failed to load registered extensions: {e}") + # A fresh connection has to be bound to the account again. + self._bind_ack = False + self._bound_address = None threading.Thread(target=self.client.start_TCP_client, daemon=True).start() self.connected = True + self._bind_account() self.server_info = { "host": params["host"], "port": params["port"], @@ -398,6 +629,246 @@ def _start_client_from_params(self, params): with open(CLIENT_LAST_SERVER_FILE, "w", encoding="utf-8") as f: json.dump({"address": self._last_address}, f, indent=4, ensure_ascii=False) + # ------------------------------------------------------- account / login + + def _server_request(self, path, payload=None): + """Send one request to the connected server web backend. + + Args: + path (str): Server route to call, e.g. ``"/api/client_login"``. + payload (dict | None): JSON body of the request; ``None`` issues a + GET without a body instead. + + Returns: + dict: Parsed JSON reply of the server. + + Raises: + ValueError: If no server address is known, the server cannot be + reached, or its reply is not a JSON object. A refused request + carries the server's own error message, or ``HTTP `` + when the server sent none. + """ + if not self._server_base: + raise ValueError("not connected to a server") + url = self._server_base + path + if payload is None: + req = urllib.request.Request(url) + else: + req = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: + body = resp.read().decode("utf-8") + except urllib.error.HTTPError as e: + raise _ServerRequestError(e.code, _http_error_message(e)) from e + except Exception as e: + raise ValueError( + f"cannot reach the server web backend at {self._server_base}: {e}" + ) from e + try: + result = json.loads(body) + except Exception as e: + raise ValueError(f"invalid server reply: {e}") from e + if not isinstance(result, dict): + raise ValueError("invalid server reply") + if result.get("ok") is False: + raise ValueError(result.get("error") or "the server refused the request") + return result + + def _save_login_file(self, identify, password, token): + """Store the credentials used to log in to this server. + + Args: + identify (str): Username or e-mail the user logged in with. + password (str): Password the user typed; the account password. + token (str): Session token the server issued for this login. + """ + payload = { + "server": self._server_base, + "identify": identify, + "password": password or "", + "token": token or "", + } + os.makedirs(FLOW_WEB_DIR, exist_ok=True) + with open(CLIENT_LOGIN_FILE, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=4, ensure_ascii=False) + try: + os.chmod(CLIENT_LOGIN_FILE, 0o600) + except OSError: + pass # best effort: file modes are not portable + + def _load_login_file(self): + """Return the saved login credentials, or ``None`` when absent.""" + if not os.path.exists(CLIENT_LOGIN_FILE): + return None + try: + with open(CLIENT_LOGIN_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + return None + return data if isinstance(data, dict) else None + + def _clear_login_file(self): + """Delete the saved login credentials, if any.""" + try: + os.remove(CLIENT_LOGIN_FILE) + except OSError: + pass + + def _forget_session(self, message="", drop_credentials=False): + """Drop the local session, keeping the saved credentials by default. + + Args: + message (str): Login error the login window explains, empty for none. + drop_credentials (bool): Delete the saved login file as well. + """ + self.session = None + self._bind_ack = False + self._bound_address = None + self._login_error = message + if drop_credentials: + self._clear_login_file() + + def _login(self, identify, password, code): + """Log in to an account of the connected server. + + A forced login always needs both factors: the account password and a + verification code mailed to the account address. + + Args: + identify (str): Username or e-mail of the account. + password (str): Password of the account. + code (str): Mailed verification code of the ``login`` purpose. + + Returns: + dict: The logged-in account without its password. + + Raises: + ValueError: If the server refuses the credentials or answers + without a session token. + """ + result = self._server_request( + "/api/client_login", {"identify": identify, "password": password, "code": code} + ) + token = str(result.get("token") or "") + if not token: + raise ValueError("the server returned no session token") + user = result.get("user") or {} + self.session = {"token": token, "user": user} + self._saved_identify = identify + self._bind_ack = False + self._bound_address = None + self._login_error = "" + self._save_login_file(identify, password, token) + self._bind_account() + return user + + def _auto_login(self): + """Restore the saved session of this server when the server accepts it. + + The saved password and session token are replayed together: the token + proves the session is known, the credentials prove they still open the + account. A file without both, or one the server refuses, leaves the + login window in charge. + """ + if self.session is not None or not self._server_base: + return + saved = self._load_login_file() + if not saved or saved.get("server") != self._server_base: + return + self._saved_identify = str(saved.get("identify") or "") + password = str(saved.get("password") or "") + token = str(saved.get("token") or "") + if not (self._saved_identify and password and token): + self._login_error = "the saved login is incomplete, sign in again" + return + payload = {"token": token, "identify": self._saved_identify, "password": password} + try: + result = self._server_request("/api/client_verify", payload) + except ValueError as e: + self._login_error = f"saved credentials were rejected: {e}" + return + self.session = {"token": token, "user": result.get("user") or {}} + self._login_error = "" + self._bind_ack = False + self._bound_address = None + self._bind_account() + + def _bind_account(self): + """Bind the TCP connection to the logged-in account of the session.""" + if self.session is None or self._bind_ack: + return + with self._bind_lock: + if self._bind_pending: + return + self._bind_pending = True + threading.Thread( + target=self._bind_loop, args=(self.session["token"],), daemon=True + ).start() + + def _bind_loop(self, token): + """Send ``/web_bind`` once a second until the server acknowledged it.""" + for attempt in range(BIND_RETRY_LIMIT): + if self._bind_ack or self.session is None: + break + if attempt: + time.sleep(BIND_RETRY_INTERVAL) + client = self.client + if not _channel_ready(client): + continue + try: + client.send_message(client.client_socket, f"{BIND_COMMAND} {token}") + except Exception: + traceback.print_exc() + with self._bind_lock: + self._bind_pending = False + + def _logout(self): + """Close the account session and forget the saved credentials.""" + token = self.session["token"] if self.session else "" + if token: + try: + self._server_request("/api/client_logout", {"token": token}) + except ValueError: + pass # the local session goes away either way + self._forget_session(drop_credentials=True) + + def _account_proxy(self, path, payload=None): + """Forward one account request to the server and shape its reply. + + Args: + path (str): Server route to call, e.g. ``"/api/contacts/search"``. + payload (dict | None): Request body without the session token; + ``None`` issues a GET carrying the token as a query argument. + + Returns: + tuple: Flask response of the request. + """ + if self.session is None: + return jsonify({"ok": False, "error": "login required"}), 401 + token = self.session["token"] + if payload is None: + body = None + path = f"{path}?token={quote(token)}" + else: + body = dict(payload) + body["token"] = token + try: + return jsonify(self._server_request(path, body)) + except _ServerRequestError as e: + status = e.status + if status == UNAUTHORIZED: + # The server dropped the session: return to the login window. + self._forget_session("the session expired, please log in again") + else: + status = 400 + return jsonify({"ok": False, "error": str(e)}), status + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + # ------------------------------------------------------------------ routes def _register_routes(self): @@ -406,15 +877,21 @@ def _register_routes(self): @app.get("/") def index(): if self.connected: - return render_template("client_main.html", mode="client") - last = "" - if os.path.exists(CLIENT_LAST_SERVER_FILE): - try: - with open(CLIENT_LAST_SERVER_FILE, "r", encoding="utf-8") as f: - last = json.load(f).get("address", "") - except Exception: - last = "" - return render_template("client_connect.html", last_address=last) + if self.session is None: + self._auto_login() + if self.session is not None: + return render_template( + "client_main.html", mode="client", user=self.session["user"] + ) + return render_template( + "client_login.html", + identify=self._saved_identify, + login_error=self._login_error, + server_address=self._server_base, + ) + return render_template( + "client_connect.html", last_address=_last_server_address() + ) @app.get("/config") def config(): @@ -470,11 +947,14 @@ def api_connect(): port = int(info.get("port")) is_enable_encrypto = bool(info.get("is_enable_encrypto", True)) self._last_address = address + self._server_base = base + self._forget_session() try: self._start_client(host, port, is_enable_encrypto) except Exception as e: traceback.print_exc() return jsonify({"ok": False, "error": f"failed to start TCP client: {e}"}), 500 + self._auto_login() return jsonify({"ok": True, "server_info": self.server_info}) @app.post("/api/save_config") @@ -505,6 +985,8 @@ def api_save_config(): @app.get("/api/status") def api_status(): + if self.session is not None and not self._bind_ack: + self._bind_account() return jsonify( { "connected": self.connected @@ -514,9 +996,86 @@ def api_status(): "clients": self._clients_snapshot(), "own_address": self._own_address(), "pid": os.getpid(), + "logged_in": self.session is not None, + "user": self.session["user"] if self.session else None, + "server_address": self._server_base, } ) + @app.post("/api/login") + def api_login(): + """Log the web client in with the account password and a mailed code.""" + data = request.get_json(force=True) + identify = str(data.get("identify") or "").strip() + password = str(data.get("password") or "") + code = str(data.get("code") or "").strip() + if not identify: + return jsonify({"ok": False, "error": "enter your user name or email"}), 400 + if not password or not code: + return ( + jsonify( + { + "ok": False, + "error": "enter the account password and the mailed verification code", + } + ), + 400, + ) + try: + user = self._login(identify, password, code) + except _ServerRequestError as e: + return jsonify({"ok": False, "error": str(e)}), e.status + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify({"ok": True, "user": user}) + + @app.post("/api/login/send_code") + def api_login_send_code(): + """Ask the server to mail a login code to one account.""" + data = request.get_json(force=True) + identify = str(data.get("identify") or "").strip() + if not identify: + return jsonify({"ok": False, "error": "enter your user name or email"}), 400 + try: + payload = self._server_request("/api/login/send_code", {"identify": identify}) + except ValueError as e: + return jsonify({"ok": False, "error": str(e)}), 400 + return jsonify(payload) + + @app.post("/api/logout") + def api_logout(): + """Close the account session and forget the saved credentials.""" + self._logout() + return jsonify({"ok": True}) + + @app.post("/api/contacts/search") + def api_contacts_search(): + """Search server accounts by user id, username or email.""" + data = request.get_json(force=True) + return self._account_proxy("/api/contacts/search", {"query": data.get("query", "")}) + + @app.post("/api/contacts/request") + def api_contacts_request(): + """Ask another account of the server to become a contact.""" + data = request.get_json(force=True) + return self._account_proxy( + "/api/contacts/request", {"user_id": data.get("user_id", "")} + ) + + @app.get("/api/contact_requests") + def api_contact_requests(): + """List the pending contact requests of the logged-in account.""" + return self._account_proxy("/api/contact_requests") + + @app.post("/api/contacts/respond") + def api_contacts_respond(): + """Accept or reject one incoming contact request.""" + data = request.get_json(force=True) + return self._account_proxy( + "/api/contacts/respond", + {"request_id": data.get("request_id"), "accept": bool(data.get("accept"))}, + ) + @app.get("/api/events") def api_events(): since = request.args.get("since", 0, type=int) @@ -641,6 +1200,28 @@ def api_sync_clients(): self.client.send_message(self.client.client_socket, "/web_sync_clients") return jsonify({"ok": True}) + @app.post("/api/ftp/list") + def api_ftp_list(): + payload = request.get_json(silent=True) or {} + try: + listing = self._ftp_list(str(payload.get("path") or "")) + except _ServerRequestError as e: + return jsonify({"ok": False, "error": str(e)}), e.status + return jsonify({"ok": True, "listing": listing}) + + @app.post("/api/ftp/download") + def api_ftp_download(): + payload = request.get_json(silent=True) or {} + wanted = payload.get("paths") + if not isinstance(wanted, list) or not wanted: + return jsonify({"ok": False, "error": "select at least one entry"}), 400 + destination = str(payload.get("destination") or "").strip() or None + try: + result = self._ftp_download([str(entry) for entry in wanted], destination) + except _ServerRequestError as e: + return jsonify({"ok": False, "error": str(e)}), e.status + return jsonify({"ok": True, **result}) + @app.get("/api/extensions_ui") def api_get_extensions_ui(): return jsonify({"extensions": _load_json_list(CLIENT_EXTENSIONS_UI_FILE)}) diff --git a/PyFlow/transfer_web/web_front/templates/client_login.html b/PyFlow/transfer_web/web_front/templates/client_login.html new file mode 100644 index 0000000..9ffc914 --- /dev/null +++ b/PyFlow/transfer_web/web_front/templates/client_login.html @@ -0,0 +1,132 @@ + + + + + + PyFlow TCP Client + + + +
+
+

Sign in to your account

+

Connected to {{ server_address }}. Enter a user name or email address, the account password and a verification code mailed to the account email.

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + + diff --git a/PyFlow/transfer_web/web_front/templates/client_main.html b/PyFlow/transfer_web/web_front/templates/client_main.html index 68a40a1..53f084e 100644 --- a/PyFlow/transfer_web/web_front/templates/client_main.html +++ b/PyFlow/transfer_web/web_front/templates/client_main.html @@ -14,8 +14,13 @@

PyFlow TCP Client

connecting...
@@ -44,7 +49,14 @@

PyFlow TCP Client

- + + + diff --git a/README.md b/README.md index c0c763d..682149f 100644 --- a/README.md +++ b/README.md @@ -123,16 +123,63 @@ uv run python PyFlow/transfer_web/setup_server.py uv run python PyFlow/transfer_web/setup_client.py ``` -On first run the server launcher opens a startup-configuration page +Visitors of the server's web address get a white landing page (the +addresses clients should connect to) with **Login**, **Register** and +**Change password** buttons; the startup-configuration page, the status +page and the web APIs behind them need a session. Accounts live in the +SQLite database `PyFlow/transfer_web/.Flow_Web/flow_web.db`: username, +email, a PBKDF2-SHA256 password record and a unique 8-character user ID +that other users search by. The first run seeds the administrator +`admin` / `admin` (a legacy `users.json` is imported once and renamed); +while that exact pair is still in use, a login pops up a prominent +warning to change the username and password **before** the server is +exposed to a public network, or anyone who can reach it can administer +it. Administrators manage users (`Users` in the sidebar, which lists the +user IDs) and are the only ones who can change the startup configuration +or load/extend extension protocols; regular users get the status page +with message/file/folder sending. `/api/server_info` stays public, as web +clients query it before they connect. + +Registration, password change and code-based client login are verified by +email: an administrator fills in the outgoing mailbox (host, port, +account, authorization code, sender and encryption) in the +startup-configuration page, the settings are checked against the real +SMTP server before they are stored in +`PyFlow/transfer_web/.Flow_Web/email_config.json`, and only then is the +verification mail service started. Codes are valid for 5 minutes, one +code may be requested per minute, and the send button counts the minute +down. Without a working mailbox nobody can register or reset a password; +the seeded administrator can still log in and configure one. + +On first run the server launcher opens the startup-configuration page showing every `TCP_Server_Base` parameter with its default; the saved config lives in `PyFlow/transfer_web/.Flow_Web/setup_server.json` (same shape as `setup.json`). Once the TCP server is up, the server's web backend serves a status page and a client-facing API (`/api/server_info` returns the TCP address/port). The client launcher asks for the server address (an `http`/`https` domain or a bare IP) and -connects through the server's web backend. Both pages show a sidebar of -connected instances, message/file/folder sending (client-to-client -sends are forwarded through the server), and extension loading. +connects through the server's web backend; it then asks the account to log +in (username/email, the account password and a verification code mailed to +the account address — both factors are required) and stores the password and +the session token in `PyFlow/transfer_web/.Flow_Web/client_login.json`, so +every reload logs the client in again until **Log out** deletes that file. +Both pages show a sidebar of connected instances and message/file/folder +sending (client-to-client sends are forwarded through the server); a web +client sees only the accounts it is a contact of. Contacts are added with +the **Contacts** button (search by user ID, username or email); the other +side answers the request in its **Requests** list, and only after both +accounts accepted each other does the contact appear in the sidebar. +Extension protocols are loaded by the client page and by administrators on +the server page. + +The server page also offers a `"ftp"` share (administrator-only). It is not +the FTP protocol: it browses one folder of the server host and hands the +ticked entries to clients over the protocol's own `/file` and `/file_folder` +transfers. The shared folder is kept in the startup configuration +(`web.ftp_root` of `setup_server.json`), so a restart keeps serving it. The +client's browse dialog takes an optional **Download to** folder: the entries +land there on the client host, and in the default transfer folder +(`PyFlow/network_api/received_files`) when the field is left empty. ### `setup.json` diff --git a/docs/DOCSTRING_GUIDE.md b/docs/DOCSTRING_GUIDE.md new file mode 100644 index 0000000..5497368 --- /dev/null +++ b/docs/DOCSTRING_GUIDE.md @@ -0,0 +1,277 @@ +# PyFlow Docstring Guide + +## 1. Scope + +- **Python**: this guide applies to every `.py` file in the repository. +- **C**: C comments follow the **Doxygen** convention (section 10). + +--- + +## 2. Style: Google docstrings + +PyFlow uses **Google style**: `sphinx.ext.napoleon` converts the Args / Returns / Raises sections into reST fields, and `docs/conf.py` enables `sphinx.ext.napoleon` + `sphinx.ext.autodoc` with `napoleon_numpy_docstring = False` (a NumPy-style docstring is rejected rather than silently accepted). No `.. automodule::` page exists yet, so docstrings only reach the built docs once API pages are added (section 11). + +> ✅ Correct: Google style (explicit Args / Returns / Raises sections) +> ❌ Forbidden: NumPy style, reST field lists (`:param:`), unstructured prose + +--- + +## 3. Required fields + +| Field | Rule | +|-------|------| +| **Summary line** | One sentence, verb first, ≤ 80 characters, on its own line | +| **Args** | One entry per parameter: `name (type): what it is and its constraints` | +| **Returns** | `type: meaning of the value and its possible values` | +| **Raises** | One entry per exception: `ExceptionType: condition that triggers it` | + +> ⚠️ Public API must carry these fields. `Returns` may be omitted when the callable returns nothing, `Raises` when it raises nothing; `Args` and `Raises` must never be dropped. + +--- + +## 4. Prohibited + +| Prohibited | Why | +|------------|-----| +| Implementation detail | No algorithm steps, data layout, caching strategy, lock granularity, third-party calls | +| Subjective wording | No "this is efficient", "elegantly designed", "carefully written" | +| Multi-line summary | The summary is a single line; it never wraps | +| Tutorials | Usage examples, best practices, architecture walkthroughs belong in `.rst` | +| Extra blank lines | One blank line between sections, none inside a section | +| Type / annotation conflict | Args must state types when the signature has none, and must match it when it has them; never restate what the signature already says | + +--- + +## 5. Visibility rules + +| Visibility | How to recognise it | Docstring requirement | +|------------|---------------------|-----------------------| +| **Public API** | No leading underscore, listed in `__all__`, imported by another module | **Full** docstring (summary + Args + Returns + Raises) | +| **Internal** | Single leading underscore, `_helper` | Summary line only, or nothing at all | +| **Private** | Double leading underscore, `__method` | Same as internal | + +> ⚠️ An internal function that is complex or easy to misuse **should** still document Args / Returns. + +--- + +## 6. Writing the summary line + +- **Verb first**: `Calculate...`, `Return...`, `Validate...`, `Load...` +- **One sentence**: no semicolons, no clauses chained with conjunctions +- **≤ 80 characters**: keep the core intent, move detail into Args / Returns + +| ❌ Counter-example | ✅ Correct | +|--------------------|-----------| +| `This function loads the RSA key from a file and returns it.` | `Load RSA private key from PEM file.` | +| `Encrypt data using OAEP padding with SHA-256.` | `Encrypt plaintext with RSA-OAEP (SHA-256).` | + +--- + +## 7. Writing parameter descriptions + +State **what the value is** and **its constraints** — never **how it is used internally**. + +| ❌ Counter-example | ✅ Correct | +|--------------------|-----------| +| `key_path: Path to the key file. The function opens it with open() and reads bytes.` | `key_path (str | Path): Path to PEM-encoded private key file. Must exist and be readable.` | +| `timeout: How long to wait. Uses socket.settimeout internally.` | `timeout (float): Maximum seconds to wait for connection. Must be > 0.` | + +--- + +## 8. Complete examples + +### 8.1 Simple function (parameters and return value only) + +```python +def calculate_checksum(data: bytes, algorithm: str = "crc32") -> int: + """Calculate checksum of binary data. + + Args: + data (bytes): Input data to checksum. Must not be empty. + algorithm (str): Checksum algorithm. One of "crc32", "adler32". + + Returns: + int: Unsigned 32-bit checksum value. + + Raises: + ValueError: If data is empty or algorithm is unknown. + """ +``` + +> PyFlow's own code carries no type annotations today (`PyFlow/` has none), so Args must state the types itself; when a signature does have annotations, Args keeps the same types (see section 4). + +### 8.2 Function that raises + +```python +def load_library(): + """Locate and load the shared crypto_api library, cached process-wide. + + Returns: + _Library: Handle exposing the ``pf_*`` ctypes bindings; the same + instance is returned on every call. + + Raises: + CryptoLibraryError: If libcrypto_api cannot be located (searched via + ``ctypes.util.find_library`` and the project's ``build/`` directory). + """ +``` + +### 8.3 Class `__init__` and public methods + +```python +class RsaCrypto: + """Key lifecycle plus RSA-OAEP encrypt/decrypt for one role. + + Attributes: + role (str): "server" or "client"; decides which key files are used. + """ + + def __init__(self, role, project_dir, ssh_dir=None, custom_keys=None): + """Create the crypto wrapper for one role. + + Args: + role (str): "server" or "client"; must match the peer's expectation. + project_dir (str): Project root holding the ``.Flow`` key directory. + ssh_dir (str | None): Extra directory searched for an existing keypair. + custom_keys (list | None): ``[pub_key_path, pvt_key_path]`` pair used + instead of the default lookup; ignored when it fails validation. + + Raises: + OSError: If the key directories cannot be created. + """ + ... + + def encrypt_for_peer(self, peer_pem_path, plaintext): + """Encrypt plaintext with the peer's stored public key. + + Args: + peer_pem_path (str): Cached PEM file holding the peer public key. + plaintext (str): Text to encrypt; chunked at no more than + key size - 2 * hash length - 2 bytes per chunk. + + Returns: + str: ASCII wire body, base64 chunks joined with ``|``, no newline. + + Raises: + ValueError: If no peer key is stored at ``peer_pem_path``. + CryptoLibraryError: If libcrypto_api or the peer key cannot be loaded. + """ + ... + + def decrypt_with_own(self, wire_body): + """Decrypt a wire body with this instance's private key. + + Args: + wire_body (str): Wire body produced by a peer's ``encrypt_for_peer``. + + Returns: + tuple: ``(True, plaintext)`` on success; ``(False, None)`` when the + key is stale or wrong, or the signature is missing. + """ + ... +``` + +--- + +## 9. Counter-example → correct example + +> The counter-example below imitates the prose style of the existing AI-written docstrings in this project. + +### ❌ Counter-example (prose, implementation detail, no structured fields) + +```python +def _exclusive_file_lock(path): + """This function provides a cross-process advisory lock using a sidecar + lock file (path + ".lock"). It uses fcntl on POSIX and msvcrt on Windows + to achieve advisory locking. The lock is implemented by opening the lock + file and acquiring an exclusive lock on the file descriptor. On Windows + we use LockFileEx with exclusive flag. The function yields control to + the caller while the lock is held, and automatically releases the lock + when the context exits. It handles the case where the lock file doesn't + exist by creating it. This is useful for preventing multiple processes + from writing to the same key file simultaneously which could cause + corruption. The implementation carefully closes the file descriptor + in a finally block to ensure no resource leaks. + """ +``` + +### ✅ Correct (Google style, structured, contract only — no implementation) + +```python +@contextlib.contextmanager +def _exclusive_file_lock(path: str | Path) -> Iterator[None]: + """Cross-process advisory lock via a sidecar file (``path + ".lock"``). + + Args: + path (str | Path): Target file to protect. Lock file is created + alongside it with ".lock" suffix. + + Yields: + None: Lock is held for the duration of the context. + + Raises: + OSError: If lock file cannot be created or lock acquisition fails. + """ +``` + +--- + +## 10. C comments: Doxygen style + +The project ships C code (`PyFlow/crypto_api/`, public headers in `include/`), and its comments follow the **Doxygen** convention. Existing headers use plain `/* */` block comments; every new or edited comment is written in Doxygen form (`@brief` mandatory, parameter order matching the declaration): + +```c +/** + * @brief Generate an RSA key pair of @p bits bits. + * + * @param[in] bits Key size in bits, 2048..16384 (2048/3072/4096 recommended). + * @param[out] out_key Receives a new handle; free it with pf_rsa_key_free(). + * @return PF_OK on success, otherwise a pf_err_t code (e.g. PF_ERR_INVALID_ARG). + * + * @note Thread-safe; a single handle must be used by one thread at a time. + */ +PF_CRYPTO_API pf_err_t pf_rsa_keygen(int bits, pf_rsa_key_t **out_key); +``` + +| Tag | Purpose | +|-----|---------| +| `@brief` | One-line summary (same rule as the Python summary line) | +| `@param[in/out]` | Parameter description, marked as input / output | +| `@return` | Meaning of the return value | +| `@note` / `@warning` | Extra contract: thread safety, side effects | +| `@see` | Related function / type | + +--- + +## 11. Working with autodoc + +- **The docstring is the single source of API detail**: every parameter, return value, exception and type constraint lives in the docstring. +- **`.rst` documents carry only**: module purpose, usage scenarios, architecture overview, tutorials, best practices, example code. +- **Never repeat in `.rst`** what the docstring already states about parameters / return values / exceptions. +- API pages are generated (not written by hand): run `sphinx-apidoc -o api -e --separate --module-first --force ../PyFlow` from `docs/` — it emits `.. automodule::`/`.. autoclass::` stubs, so the single source stays authoritative and the pages cannot drift from the docstrings. +- **Migration**: existing `.rst` files under `docs/` are left untouched for now; new or rewritten API descriptions go into the docstring per this guide, and pre-existing parameter detail is moved out of `.rst` over time. + +--- + +## 12. Continuous checks + +CI job `Docstrings` (`.github/workflows/ci.yml`) runs the same configuration locally available: + +| Check | Command | What it reads | Blocking | +|-------|---------|---------------|----------| +| Style | `uvx ruff check` | `D` rules configured in `pyproject.toml`: summary line, blank lines, imperative mood, terminal punctuation, incomplete `Args`, missing docstrings on public API | No (`continue-on-error`, like the other lint steps) | +| Coverage | `uvx interrogate` | Public API of `PyFlow/` (`fail-under` in `pyproject.toml`; raise it with every batch of docstrings) | Yes | + +Known limits — check these by hand in review: + +- `D417` fires only when an `Args` section exists and skips a parameter; a function whose parameters have **no** `Args` section at all still passes. +- Missing-docstring rules (`D1`) are off under `test/**`: tests are not public API. +- Nothing verifies that `.rst` pages and docstrings do not duplicate each other (section 11). +- Reaching 100% is not required: `D1` is off for private and internal callables by design (section 5), so a pass only means the *public* surface is documented. +- The coverage ratchet ignores `_private` / `__private` names and reads a class plus its `__init__` as one unit (`style = "google"`), matching section 5. + +Raise `fail-under` to the newly measured value in the same change that adds the docstrings; never lower it. + +--- + +> Maintainers: when this guide changes, update the documentation checklist in the PR template in the same change. diff --git a/docs/Instance_Setup/Instance_Setup.rst b/docs/Instance_Setup/Instance_Setup.rst index 40ea353..d842054 100644 --- a/docs/Instance_Setup/Instance_Setup.rst +++ b/docs/Instance_Setup/Instance_Setup.rst @@ -66,17 +66,21 @@ Command‑line Mode Use the following options: -+----------------------+-------------------------------------------------------+ -| Option | Description | -+======================+=======================================================+ -| ``--type {0,1}`` | **Required.** 0 = Server, 1 = Client. | -+----------------------+-------------------------------------------------------+ -| ``--setup_addr_port``| **Required.** Bind address and port (e.g. ``127.0.0.1:8000``). | -+----------------------+-------------------------------------------------------+ -| ``--connect_addr_port``| Required for Client only. Server address and port to connect to. | -+----------------------+-------------------------------------------------------+ -| ``--setup_num`` | *Ignored.* The script always launches a single instance. This flag is accepted for compatibility but has no effect. | -+----------------------+-------------------------------------------------------+ ++--------------------------+---------------------------------------------------+ +| Option | Description | ++==========================+===================================================+ +| ``--type {0,1}`` | **Required.** 0 = Server, 1 = Client. | ++--------------------------+---------------------------------------------------+ +| ``--setup_addr_port`` | **Required.** Bind address and port | +| | (e.g. ``127.0.0.1:8000``). | ++--------------------------+---------------------------------------------------+ +| ``--connect_addr_port`` | Required for Client only. Server address and | +| | port to connect to. | ++--------------------------+---------------------------------------------------+ +| ``--setup_num`` | *Ignored.* The script always launches a single | +| | instance. This flag is accepted for | +| | compatibility but has no effect. | ++--------------------------+---------------------------------------------------+ Examples -------- @@ -154,12 +158,12 @@ loaded automatically for every instance whose ``setup.json`` entry sets ``is_extend_command=True``: - ``command_control_extension_tcp.py`` – remote command -execution with per-client log collection (``/command``). + execution with per-client log collection (``/command``). - ``forward_extension_tcp.py`` – forwarding files, -multiple files, folders and multiple folders to any -number of destination clients (``/file_forward``, -``/multiple_file_forward``, ``/folder_forward``, -``/multiple_folder_forward``). + multiple files, folders and multiple folders to any + number of destination clients (``/file_forward``, + ``/multiple_file_forward``, ``/folder_forward``, + ``/multiple_folder_forward``). Plain-message forwarding is native to the TCP protocol (no extension needed): the client-only command @@ -173,11 +177,11 @@ The ``is_input_command_in_console`` flag selects how the instance is started: - ``True`` (default) – ``start_TCP_Server()`` / -``start_TCP_client()`` is called directly and the -console input loop runs in its own thread. + ``start_TCP_client()`` is called directly and the + console input loop runs in its own thread. - ``False`` – the instance runs in a background thread -and the launcher keeps the process alive until the -instance stops (useful for headless deployments). + and the launcher keeps the process alive until the + instance stops (useful for headless deployments). Both extensions also expose injectable registration (``setup_server_commands(instance)`` / @@ -190,12 +194,12 @@ can be loaded onto the same instance from code. Internal Operation ================== -- Each instance is launched in a new terminal window -- (or background process). -- The configuration is passed via a temporary JSON -- file to avoid shell escaping issues. -- If an instance fails to start, the error is -- displayed and the window pauses for inspection. +- Each instance is launched in a new terminal window + (or background process). +- The configuration is passed via a temporary JSON + file to avoid shell escaping issues. +- If an instance fails to start, the error is + displayed and the window pauses for inspection. Requirements ============ diff --git a/docs/MAP.md b/docs/MAP.md new file mode 100644 index 0000000..a93a1eb --- /dev/null +++ b/docs/MAP.md @@ -0,0 +1,57 @@ +# Documentation Map + +> This file is a skeleton; content is still being filled in. + +## Documentation layout + +| Path | Purpose | Status | +|------|---------|--------| +| `docs/` | Sphinx reST documentation root | ✅ present | +| `docs/conf.py` | Sphinx configuration | ✅ present | +| `docs/templates/` | Change-note, design-note and how-to templates | ✅ present | +| `docs/changes/` | Auto-generated per-PR change notes | ❌ not created yet | +| `docs/design/` | Design notes (ADR) archive | ❌ not created yet | +| `docs/MAP.md` | This file: the documentation map | ✅ this file | + +## Normative documents + +| File | Purpose | +|------|---------| +| `docs/DOCSTRING_GUIDE.md` | Python docstring rules (Google style) plus C comment rules (Doxygen) | +| `docs/templates/adr.md` | Design-note template → `docs/design/` | +| `docs/templates/changelog-entry.md` | Changelog entry template → CHANGELOG | +| `docs/templates/change-note.md` | Per-PR change note template → `docs/changes/` | +| `docs/templates/how-to.md` | How-to guide template | +| `.github/PULL_REQUEST_TEMPLATE.md` | PR template: original checklist plus documentation impact / design decisions | + +## Existing .rst documents + +| File | Purpose | +|------|---------| +| `docs/index.rst` | Documentation entry point | +| `docs/Instance_Setup/Instance_Setup.rst` | Instance setup | +| `docs/Crypto/Crypto.rst` | Crypto module | +| `docs/File_Transfer/File_Transfer.rst` | File transfer | +| `docs/Network_APIs/TCP_Server_APIs.rst` | TCP server APIs | +| `docs/Network_APIs/TCP_Client_APIs.rst` | TCP client APIs | +| `docs/Network_APIs/UDP_APIs.rst` | UDP APIs | +| `docs/Port_Allocation/Port_Allocation.rst` | Port allocation | + +## Translations + +`locale/` holds the gettext catalogues (`.po`): zh_TW, zh_CN, ru, ko, ja. + +## Known gaps + +- `docs/changes/` and `docs/design/` do not exist yet (their templates are ready). +- API pages under `docs/api/` are generated by `sphinx-apidoc` (`-T`, so it writes no `modules.rst`); rerun it after adding or removing a *module*. The entry point is the hand-written `docs/api/index.rst` ("API Reference"), linked from the main toctree; page options live in `autodoc_default_options` in `docs/conf.py`. +- Docstring backlog: 106 open ruff `D` findings (59 of them undocumented public API); public-API coverage measured by interrogate is 66.1%, gated by `fail-under = 65.0` in `pyproject.toml`. +- No Doxygen configuration (no `Doxyfile`); `PyFlow/crypto_api/include/` uses plain block comments. +- Pre-existing docs warnings: `docs/_static` missing, duplicate label `public-api-summary` at `Port_Allocation.rst:287`, `UDP_APIs.rst` in no toctree. + +## To be filled in + +- [ ] Drift guard for `docs/api/`: rerun `sphinx-apidoc` in CI and fail when the result differs +- [ ] Raise the interrogate ratchet as the remaining public-API docstrings get written +- [ ] Install / launch / extension-development guides based on `docs/templates/how-to.md` +- [ ] Move parameter detail out of the existing `.rst` files into docstrings (see `DOCSTRING_GUIDE.md` section 11) diff --git a/docs/Network_APIs/TCP_Client_APIs.rst b/docs/Network_APIs/TCP_Client_APIs.rst index ca57dba..3cf88e5 100644 --- a/docs/Network_APIs/TCP_Client_APIs.rst +++ b/docs/Network_APIs/TCP_Client_APIs.rst @@ -25,7 +25,10 @@ the TCP server and exchange data with the server. max_custom_workers: Any, is_extend_command: Any=False, is_enable_encrypto: Any=True, - is_custom_keys: Any=None) -> None: + is_custom_keys: Any=None, + max_mem_buff: Any=2048, + is_debug: Any=False, + is_print_log: Any=True) -> None: ... The TCP Client Setup API is defined in the ``TCP_Client_Base`` class. @@ -47,6 +50,11 @@ The parameters of the ``__init__`` method are as follows: user-supplied RSA keys. Both files must exist, parse as PEM, and pair with each other; an invalid pair is silently ignored and the default key lookup is used instead (``None`` keeps the default lookup). +- ``is_debug``: A flag selecting how much detail is logged. With ``False`` + (the default) the client logs command content and execution results only; + with ``True`` it also logs the key steps of the execution process. +- ``is_print_log``: A flag indicating whether the client logs at all. With + ``False`` it prints nothing, whatever ``is_debug`` says. Every parameter has default values: @@ -68,6 +76,13 @@ Every parameter has default values: - ``is_custom_keys``: Default is ``None`` (a ``[pub_key_path, pvt_key_path]`` list uses a user-supplied keypair instead of the default ``~/.ssh`` / generated keys) +- ``is_debug``: Default is ``False`` + (``True`` adds the execution-process lines to the command/result lines) +- ``is_print_log``: Default is ``True`` + (``False`` silences every line the client would print) +- ``max_mem_buff``: Default is ``2048`` + (kept for parity with the server class; the client's forward path does not + read it today) The TCP Client Setup API will initialize all the necessary parameters and resources for the TCP client. @@ -200,7 +215,7 @@ In `handle_server_command`, the client processes built-in commands sent by the server, such as: - ``/client_alloc_port_range``: configures the client's manual - port allocation range based on server broadcast. + port allocation range from the line the server sends it on connect. - ``/server_file_transfer_port``: receives the file transfer port assigned by the server for an ongoing file operation. - ``/file`` and ``/file_folder``: handle file transfer requests @@ -655,8 +670,9 @@ of the server. They allow you to allocate ephemeral ports either automatically (by returning 0, letting the OS choose) or manually within a configured range. -To change the port allocation mode, the client listens to -the server's broadcast of ``/client_alloc_port_range``. +To configure the port allocation mode, the client reads the +``/client_alloc_port_range`` line the server sends it right after +the connection is accepted. When the server sends that command with a number, the client sets ``self.is_hand_alloc_port = True`` and configures the range. If the server sends ``NO_LIMIT``, the client uses diff --git a/docs/Network_APIs/TCP_Server_APIs.rst b/docs/Network_APIs/TCP_Server_APIs.rst index 89ffe09..491cf1f 100644 --- a/docs/Network_APIs/TCP_Server_APIs.rst +++ b/docs/Network_APIs/TCP_Server_APIs.rst @@ -25,7 +25,11 @@ clients. max_custom_workers: Any, is_extend_command: Any=False, is_enable_encrypto: Any=True, - is_custom_keys: Any=None) -> None: + is_custom_keys: Any=None, + max_mem_buff: Any=2048, + is_asynic_clients_io: Any=False, + is_debug: Any=False, + is_print_log: Any=True) -> None: ... The TCP Server Setup API is defined in the ``TCP_Server_Base`` class. @@ -33,7 +37,8 @@ The parameters of the ``__init__`` method are as follows: - ``host``: The host IP address to bind the TCP server to. - ``port``: The port number to bind the TCP server to. -- ``max_clients``: The maximum number of concurrent clients the server can handle. +- ``max_clients``: The maximum number of concurrent clients the server can handle + (ignored when ``is_asynic_clients_io`` is ``True``). - ``port_add_step``: The step size for incrementing the port number. - ``port_range_num``: The number of ports to check in the range. - ``max_file_transfer_thread_num``: The maximum number of threads for file transfer operations. @@ -46,6 +51,15 @@ The parameters of the ``__init__`` method are as follows: user-supplied RSA keys. Both files must exist, parse as PEM, and pair with each other; an invalid pair is silently ignored and the default key lookup is used instead (``None`` keeps the default lookup). +- ``is_asynic_clients_io``: A flag indicating whether clients are served by + asyncio coroutines on one event loop instead of one thread per client. It + lifts the ``max_clients`` limit, so a single server can hold thousands of + concurrent connections. +- ``is_debug``: A flag selecting how much detail is logged. With ``False`` + (the default) the server logs command content and execution results only; + with ``True`` it also logs the key steps of the execution process. +- ``is_print_log``: A flag indicating whether the server logs at all. With + ``False`` it prints nothing, whatever ``is_debug`` says. All parameters have default values: @@ -66,6 +80,13 @@ All parameters have default values: - ``is_custom_keys``: Default is ``None`` (a ``[pub_key_path, pvt_key_path]`` list uses a user-supplied keypair instead of the default ``~/.ssh`` / generated keys) +- ``is_asynic_clients_io``: Default is ``False`` + (when ``True``, every accepted connection is served by a coroutine on one + asyncio event loop, and ``max_clients`` is ignored) +- ``is_debug``: Default is ``False`` + (``True`` adds the execution-process lines to the command/result lines) +- ``is_print_log``: Default is ``True`` + (``False`` silences every line the server would print) The TCP Server Setup API will initialize all the necessary parameters and resources for the TCP server. @@ -100,6 +121,11 @@ successfully created. The ``self.running`` variable is used to control the main loop of the TCP server, and it will be set to ``False`` when the server is shutting down.* +With ``is_asynic_clients_io=True`` the main loop is the asyncio event loop +instead: it accepts clients in non-blocking mode and schedules one coroutine +per connection, it never applies the ``max_clients`` limit, and `stop` +releases it so the loop can end. + The main loop of the TCP server setup function first checks whether the number of connected clients exceeds the maximum number of clients. The maximum number of clients is defined by the ``self.max_clients`` argument of the @@ -195,7 +221,8 @@ connection is accepted and is responsible for: - adding the client entry into ``self.clients`` with socket, address, id, and connected time - printing connection information and current client count - sending a welcome message to the client -- broadcasting ``/client_alloc_port_range`` information to all clients depending on port allocation mode +- sending the ``/client_alloc_port_range`` information to that client depending + on the port allocation mode *Note: You can specify the port allocation mode in the arguments which have been defined in the diff --git a/docs/api/PyFlow.add_extension.rst b/docs/api/PyFlow.add_extension.rst new file mode 100644 index 0000000..a3eb4cd --- /dev/null +++ b/docs/api/PyFlow.add_extension.rst @@ -0,0 +1,7 @@ +PyFlow.add\_extension module +============================ + +.. automodule:: PyFlow.add_extension + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.command_control_extension_tcp.rst b/docs/api/PyFlow.command_control_extension_tcp.rst new file mode 100644 index 0000000..e09feee --- /dev/null +++ b/docs/api/PyFlow.command_control_extension_tcp.rst @@ -0,0 +1,7 @@ +PyFlow.command\_control\_extension\_tcp module +============================================== + +.. automodule:: PyFlow.command_control_extension_tcp + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.flow_setup.rst b/docs/api/PyFlow.flow_setup.rst new file mode 100644 index 0000000..0d60937 --- /dev/null +++ b/docs/api/PyFlow.flow_setup.rst @@ -0,0 +1,7 @@ +PyFlow.flow\_setup module +========================= + +.. automodule:: PyFlow.flow_setup + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.forward_extension_tcp.rst b/docs/api/PyFlow.forward_extension_tcp.rst new file mode 100644 index 0000000..90c5136 --- /dev/null +++ b/docs/api/PyFlow.forward_extension_tcp.rst @@ -0,0 +1,7 @@ +PyFlow.forward\_extension\_tcp module +===================================== + +.. automodule:: PyFlow.forward_extension_tcp + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.network_api.connect_tcp.rst b/docs/api/PyFlow.network_api.connect_tcp.rst new file mode 100644 index 0000000..b29a548 --- /dev/null +++ b/docs/api/PyFlow.network_api.connect_tcp.rst @@ -0,0 +1,7 @@ +PyFlow.network\_api.connect\_tcp module +======================================= + +.. automodule:: PyFlow.network_api.connect_tcp + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.network_api.connect_udp.rst b/docs/api/PyFlow.network_api.connect_udp.rst new file mode 100644 index 0000000..572c3c0 --- /dev/null +++ b/docs/api/PyFlow.network_api.connect_udp.rst @@ -0,0 +1,7 @@ +PyFlow.network\_api.connect\_udp module +======================================= + +.. automodule:: PyFlow.network_api.connect_udp + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.network_api.rsa_crypto.rst b/docs/api/PyFlow.network_api.rsa_crypto.rst new file mode 100644 index 0000000..041bd79 --- /dev/null +++ b/docs/api/PyFlow.network_api.rsa_crypto.rst @@ -0,0 +1,7 @@ +PyFlow.network\_api.rsa\_crypto module +====================================== + +.. automodule:: PyFlow.network_api.rsa_crypto + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.network_api.rst b/docs/api/PyFlow.network_api.rst new file mode 100644 index 0000000..ec09f87 --- /dev/null +++ b/docs/api/PyFlow.network_api.rst @@ -0,0 +1,17 @@ +PyFlow.network\_api package +=========================== + +.. automodule:: PyFlow.network_api + :members: + :show-inheritance: + :undoc-members: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.network_api.connect_tcp + PyFlow.network_api.connect_udp + PyFlow.network_api.rsa_crypto diff --git a/docs/api/PyFlow.rst b/docs/api/PyFlow.rst new file mode 100644 index 0000000..e6fe34b --- /dev/null +++ b/docs/api/PyFlow.rst @@ -0,0 +1,27 @@ +PyFlow package +============== + +.. automodule:: PyFlow + :members: + :show-inheritance: + :undoc-members: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.network_api + PyFlow.transfer_web + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.add_extension + PyFlow.command_control_extension_tcp + PyFlow.flow_setup + PyFlow.forward_extension_tcp diff --git a/docs/api/PyFlow.transfer_web.rst b/docs/api/PyFlow.transfer_web.rst new file mode 100644 index 0000000..1831c74 --- /dev/null +++ b/docs/api/PyFlow.transfer_web.rst @@ -0,0 +1,25 @@ +PyFlow.transfer\_web package +============================ + +.. automodule:: PyFlow.transfer_web + :members: + :show-inheritance: + :undoc-members: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.transfer_web.web_backend + PyFlow.transfer_web.web_front + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.transfer_web.setup_client + PyFlow.transfer_web.setup_server diff --git a/docs/api/PyFlow.transfer_web.setup_client.rst b/docs/api/PyFlow.transfer_web.setup_client.rst new file mode 100644 index 0000000..fb1564a --- /dev/null +++ b/docs/api/PyFlow.transfer_web.setup_client.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.setup\_client module +========================================= + +.. automodule:: PyFlow.transfer_web.setup_client + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.setup_server.rst b/docs/api/PyFlow.transfer_web.setup_server.rst new file mode 100644 index 0000000..cc7d60d --- /dev/null +++ b/docs/api/PyFlow.transfer_web.setup_server.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.setup\_server module +========================================= + +.. automodule:: PyFlow.transfer_web.setup_server + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.web_backend.mail_service.rst b/docs/api/PyFlow.transfer_web.web_backend.mail_service.rst new file mode 100644 index 0000000..3321a25 --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_backend.mail_service.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.web\_backend.mail\_service module +====================================================== + +.. automodule:: PyFlow.transfer_web.web_backend.mail_service + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.web_backend.rst b/docs/api/PyFlow.transfer_web.web_backend.rst new file mode 100644 index 0000000..ed302d2 --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_backend.rst @@ -0,0 +1,17 @@ +PyFlow.transfer\_web.web\_backend package +========================================= + +.. automodule:: PyFlow.transfer_web.web_backend + :members: + :show-inheritance: + :undoc-members: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.transfer_web.web_backend.mail_service + PyFlow.transfer_web.web_backend.server_backend + PyFlow.transfer_web.web_backend.user_database diff --git a/docs/api/PyFlow.transfer_web.web_backend.server_backend.rst b/docs/api/PyFlow.transfer_web.web_backend.server_backend.rst new file mode 100644 index 0000000..fd9d290 --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_backend.server_backend.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.web\_backend.server\_backend module +======================================================== + +.. automodule:: PyFlow.transfer_web.web_backend.server_backend + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.web_backend.user_database.rst b/docs/api/PyFlow.transfer_web.web_backend.user_database.rst new file mode 100644 index 0000000..aef35ac --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_backend.user_database.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.web\_backend.user\_database module +======================================================= + +.. automodule:: PyFlow.transfer_web.web_backend.user_database + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.web_front.client_backend.rst b/docs/api/PyFlow.transfer_web.web_front.client_backend.rst new file mode 100644 index 0000000..8d6736d --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_front.client_backend.rst @@ -0,0 +1,7 @@ +PyFlow.transfer\_web.web\_front.client\_backend module +====================================================== + +.. automodule:: PyFlow.transfer_web.web_front.client_backend + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/PyFlow.transfer_web.web_front.rst b/docs/api/PyFlow.transfer_web.web_front.rst new file mode 100644 index 0000000..527dbe2 --- /dev/null +++ b/docs/api/PyFlow.transfer_web.web_front.rst @@ -0,0 +1,15 @@ +PyFlow.transfer\_web.web\_front package +======================================= + +.. automodule:: PyFlow.transfer_web.web_front + :members: + :show-inheritance: + :undoc-members: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + PyFlow.transfer_web.web_front.client_backend diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..4c7de74 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,11 @@ +API Reference +============= + +The pages below are generated from the code by ``sphinx-apidoc`` (see the first +line of ``docs/reBuild.sh``): each one pulls its text from the docstrings at build +time, so nothing here is written by hand. + +.. toctree:: + :maxdepth: 4 + + PyFlow diff --git a/docs/batch_translate_po.py b/docs/batch_translate_po.py old mode 100644 new mode 100755 index d41e925..e5a8f7a --- a/docs/batch_translate_po.py +++ b/docs/batch_translate_po.py @@ -1,26 +1,108 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -""" -批量翻译 Sphinx 项目的 .po 文件到多种语言。 -支持递归处理深层 .po 文件,带进度显示和超时控制。 +"""Batch-translate the .po files of this Sphinx project into several languages. + +Walks ``locale//LC_MESSAGES/**/*.po`` recursively and translates every +entry whose ``msgstr`` is still empty. A failed entry keeps its empty ``msgstr``, +so running the script again continues where it left off. + +Engines (``--engine``) + ``mymemory`` MyMemory REST API — **default, no key at all**. Reachable without a proxy + (verified from a mainland connection), so it needs neither an account nor a + proxy. Anonymous quota is 5,000 characters per day per IP, which covers this + project's ~4.8k characters per pass; set ``MYMEMORY_EMAIL`` (any address, no + registration) to raise it to 50,000. Quality is translation-memory grade. + ``baidu`` Baidu Translate open API — needs ``BAIDU_APPID`` plus ``BAIDU_KEY`` + (deep-translator's ``BAIDU_APPKEY`` is accepted too) and real-name + registration. Domestic, so it needs no proxy. Free standard tier is 50k + characters per day. Better quality than MyMemory once a key exists. + deep-translator's own ``BaiduTranslator`` has the same requirement. + ``microsoft`` Microsoft Translator v3. ``MICROSOFT_API_KEY`` plus + ``MICROSOFT_API_REGION`` (aliases accepted: ``MICROSOFT_TRANSLATOR_*``, + ``AZURE_TRANSLATOR_*``); the key name matches deep-translator's own + ``MICROSOFT_API_KEY``. Needs the proxy to leave the country, but a keyed API + is never treated as scraping. Free F0 tier is 2M characters per month. + Implemented with a direct call rather than deep-translator's + ``MicrosoftTranslator``: that class rejects every Chinese target code + (``zh-Hans``/``zh-Hant``/``zh-cn``/``zh-tw``/names all raise + ``LanguageNotSupportedException``), and Chinese is half of our targets. + ``google`` deep-translator's Google web endpoint. It is **not** a rate-limit problem + that a different proxy node can fix: Google answers automated clients with + its "Sorry..." anti-abuse page (HTTP 429) regardless of User-Agent or exit + IP, while the same IP loads google.com and translate.google.com normally. + +The generated API pages (``api/*.po``) are skipped by default: they are docstrings pulled +in by autodoc, and keeping them English keeps the API text single-sourced (see +``docs/DOCSTRING_GUIDE.md`` section 11) — they also account for ~97% of the characters. +Use ``--include-generated`` to translate them anyway. + +Rate limits and failures are handled instead of aborting the run: +requests are paced by ``REQUEST_DELAY``; a rate-limited entry is retried after +``RATE_LIMIT_BACKOFF`` seconds; after ``MAX_CONSECUTIVE_FAILURES`` failures in a row the run +stops with an explanation. + +Usage: + python3 batch_translate_po.py # every language, mymemory (no key, no proxy) + python3 batch_translate_po.py --engine baidu # better quality once BAIDU_APPID/KEY exist + python3 batch_translate_po.py --lang ja # a single language + python3 batch_translate_po.py --limit 5 # at most 5 entries per file + python3 batch_translate_po.py --proxy http://127.0.0.1:7897 # route via an explicit proxy """ +import argparse +import os import sys import time +from hashlib import md5 +from importlib.util import find_spec from pathlib import Path +from urllib.parse import urlparse + +import requests # ========== 用户配置 ========== LANGUAGES = ["ja", "zh_CN", "zh_TW", "ko", "ru"] SOURCE_LANG = "en" -REQUEST_DELAY = 0.8 # 每条翻译间隔(秒),防限流 +ENGINE = "mymemory" # mymemory (默认,免 key) | baidu | microsoft | google +TRANSLATE_GENERATED_PAGES = False # api/*.po 是 docstring 生成的,默认保持英文(单源) +REQUEST_DELAY = 0.5 # 两次请求之间的最小间隔(秒) REQUEST_TIMEOUT = 30 # 单条请求超时(秒) MAX_RETRIES = 3 # 单条翻译失败重试次数 +RATE_LIMIT_BACKOFF = (10, 30) # 被限流后的退避秒数,按尝试次数递增 +RATE_LIMIT_MARKERS = ("too many requests", "server error", "429", "quota") +MAX_CONSECUTIVE_FAILURES = 3 # 连续失败达到该数量即停止本轮 ENABLE_TRANSLATION = True -LANG_MAP = { +GOOGLE_LANG = { + "zh_CN": "zh-CN", + "zh_TW": "zh-TW", +} +BAIDU_LANG = { + "ja": "jp", + "ko": "kor", + "ru": "ru", + "zh_CN": "zh", + "zh_TW": "cht", +} +MICROSOFT_LANG = { + "ja": "ja", + "ko": "ko", + "ru": "ru", + "zh_CN": "zh-Hans", + "zh_TW": "zh-Hant", +} +MYMEMORY_LANG = { + "ja": "ja", + "ko": "ko", + "ru": "ru", "zh_CN": "zh-CN", "zh_TW": "zh-TW", } +MYMEMORY_URL = "https://api.mymemory.translated.net/get" +MYMEMORY_QUERY_LIMIT = 450 # bytes per query (the API caps a query at 500) +MYMEMORY_OK_STATUS = 200 # the API reports failures through responseStatus +BAIDU_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate" +MICROSOFT_URL = "https://api.cognitive.microsofttranslator.com/translate" # ================================ try: @@ -31,7 +113,384 @@ sys.exit(1) -def find_locale_dir(start_path: Path) -> Path: +class ConfigError(RuntimeError): + """Raised when the selected engine is missing its credentials.""" + + +class RateLimitAbort(RuntimeError): + """Raised when consecutive failures show that the endpoint refuses the client.""" + + +class Throttle: + """Pace the translate requests across files and languages.""" + + def __init__(self, delay: float = REQUEST_DELAY): + """Initialize the pacer. + + Args: + delay (float): Minimum seconds between two requests; 0 disables waiting. + """ + self.delay = delay + self._last = 0.0 + + def wait(self): + """Sleep until the next request is allowed to go out.""" + gap = self.delay - (time.monotonic() - self._last) + if gap > 0: + time.sleep(gap) + self._last = time.monotonic() + + +class GoogleEngine: + """Translate through deep-translator's Google web endpoint.""" + + def __init__(self, target_lang: str, proxy: str | None): + """Initialize the engine. + + Args: + target_lang (str): Target locale such as "ja" or "zh_CN". + proxy (str | None): Explicit proxy URL, or None for the environment setting. + """ + self._translator = GoogleTranslator( + source=SOURCE_LANG, + target=GOOGLE_LANG.get(target_lang, target_lang), + timeout=REQUEST_TIMEOUT, + proxies=proxy_map(proxy), + ) + + def translate(self, text: str) -> str: + """Translate one string. + + Args: + text (str): Text to translate. + + Returns: + str: The translation. + """ + return self._translator.translate(text) + + +class BaiduEngine: + """Translate through the Baidu open API (domestic, works without a proxy).""" + + def __init__(self, target_lang: str, proxy: str | None): + """Initialize the engine. + + Args: + target_lang (str): Target locale such as "ja" or "zh_CN". + proxy (str | None): Explicit proxy URL, or None for the environment setting. + + Raises: + ConfigError: When the appid or the key is not set (``BAIDU_APPID`` plus + ``BAIDU_KEY``, or deep-translator's ``BAIDU_APPKEY``). + """ + self.appid = os.environ.get("BAIDU_APPID", "") + self.key = os.environ.get("BAIDU_KEY") or os.environ.get("BAIDU_APPKEY", "") + if not (self.appid and self.key): + raise ConfigError( + "缺少 BAIDU_APPID / BAIDU_KEY(或 BAIDU_APPKEY):" + "fanyi-api.baidu.com 申请,标准版免费 5 万字符/天" + ) + if target_lang not in BAIDU_LANG: + raise ConfigError(f"百度引擎不支持目标语言: {target_lang}") + self.target = BAIDU_LANG[target_lang] + self.proxies = proxy_map(proxy) + + def translate(self, text: str) -> str: + """Translate one string. + + Args: + text (str): Text to translate. + + Returns: + str: The translation; translated text may come back in several parts. + + Raises: + RuntimeError: When the API answers with an error payload. + """ + salt = str(int(time.time() * 1000) % 100000) + sign = md5((self.appid + text + salt + self.key).encode("utf-8")).hexdigest() + response = requests.post( + BAIDU_URL, + data={ + "q": text, + "from": SOURCE_LANG, + "to": self.target, + "appid": self.appid, + "salt": salt, + "sign": sign, + }, + proxies=self.proxies, + timeout=REQUEST_TIMEOUT, + ) + payload = response.json() + if "trans_result" not in payload: + raise RuntimeError(f"baidu api error: {payload}") + return "".join(part["dst"] for part in payload["trans_result"]) + + +def env_value(*names: str) -> str: + """Return the first environment variable among ``names`` that is set and non-empty. + + Args: + *names (str): Candidate variable names, highest priority first. + + Returns: + str: The value found, or "" when none of them is set. + """ + for name in names: + value = os.environ.get(name) + if value: + return value + return "" + + +class MicrosoftEngine: + """Translate through the Microsoft Translator v3 API (needs a key and the region).""" + + def __init__(self, target_lang: str, proxy: str | None): + """Initialize the engine. + + Args: + target_lang (str): Target locale such as "ja" or "zh_CN". + proxy (str | None): Explicit proxy URL, or None for the environment setting. + + Raises: + ConfigError: When the key, the region or the target locale is missing. + """ + self.key = env_value( + "MICROSOFT_API_KEY", "MICROSOFT_TRANSLATOR_KEY", "AZURE_TRANSLATOR_KEY" + ) + self.region = env_value( + "MICROSOFT_API_REGION", "MICROSOFT_TRANSLATOR_REGION", "AZURE_TRANSLATOR_REGION" + ) + if not (self.key and self.region): + raise ConfigError( + "缺少 MICROSOFT_API_KEY / MICROSOFT_API_REGION" + "(Azure 门户建 Translator 资源即可,免费 F0 层 200 万字符/月)" + ) + if target_lang not in MICROSOFT_LANG: + raise ConfigError(f"microsoft 引擎不支持目标语言: {target_lang}") + self.target = MICROSOFT_LANG[target_lang] + self.proxies = proxy_map(proxy) + + def translate(self, text: str) -> str: + """Translate one string. + + Args: + text (str): Text to translate. + + Returns: + str: The translation. + + Raises: + RuntimeError: When the API answers with an error payload. + """ + response = requests.post( + MICROSOFT_URL, + params={"api-version": "3.0", "from": SOURCE_LANG, "to": self.target}, + headers={ + "Ocp-Apim-Subscription-Key": self.key, + "Ocp-Apim-Subscription-Region": self.region, + "Content-Type": "application/json", + }, + json=[{"Text": text}], + proxies=self.proxies, + timeout=REQUEST_TIMEOUT, + ) + payload = response.json() + if not isinstance(payload, list) or "translations" not in payload[0]: + raise RuntimeError(f"azure api error: {payload}") + return payload[0]["translations"][0]["text"] + + +def split_query(text: str, limit: int = MYMEMORY_QUERY_LIMIT): + """Split a long string into chunks the translation API accepts per query. + + Args: + text (str): Text to split, usually a ``msgid``. + limit (int): Maximum bytes per chunk. + + Returns: + list[str]: One chunk when the text fits, otherwise word-aligned chunks. + """ + if len(text.encode("utf-8")) <= limit: + return [text] + chunks = [] + current = "" + for word in text.split(" "): + candidate = f"{current} {word}".strip() + if current and len(candidate.encode("utf-8")) > limit: + chunks.append(current) + current = word + else: + current = candidate + if current: + chunks.append(current) + return chunks + + +class MyMemoryEngine: + """Translate through the keyless MyMemory API (no proxy, no credentials).""" + + def __init__(self, target_lang: str, proxy: str | None): + """Initialize the engine. + + Args: + target_lang (str): Target locale such as "ja" or "zh_CN". + proxy (str | None): Explicit proxy URL, or None for the environment setting. + + Raises: + ConfigError: When the target locale is not supported. + """ + if target_lang not in MYMEMORY_LANG: + raise ConfigError(f"MyMemory 不支持目标语言: {target_lang}") + self.target = MYMEMORY_LANG[target_lang] + self.proxies = proxy_map(proxy) + self.email = os.environ.get("MYMEMORY_EMAIL", "") # raises the daily quota to 50k + + def translate(self, text: str) -> str: + """Translate one string, splitting it when it exceeds the per-query limit. + + Args: + text (str): Text to translate. + + Returns: + str: The translation. + + Raises: + RuntimeError: When the API reports a failure (quota exhausted included). + """ + return " ".join(self._translate_chunk(chunk) for chunk in split_query(text)) + + def _translate_chunk(self, chunk: str) -> str: + """Translate a single chunk that fits the per-query limit. + + Args: + chunk (str): Text of at most ``MYMEMORY_QUERY_LIMIT`` bytes. + + Returns: + str: The translation. + + Raises: + RuntimeError: When the API reports a failure. + """ + params = {"q": chunk, "langpair": f"{SOURCE_LANG}|{self.target}"} + if self.email: + params["de"] = self.email + response = requests.get( + MYMEMORY_URL, params=params, proxies=self.proxies, timeout=REQUEST_TIMEOUT + ) + payload = response.json() + translated = (payload.get("responseData") or {}).get("translatedText") or "" + failed = payload.get("responseStatus") != MYMEMORY_OK_STATUS + if failed or "MYMEMORY WARNING" in translated.upper(): + raise RuntimeError(f"mymemory api error: {payload.get('responseDetails') or payload}") + return translated + + +ENGINES = { + "mymemory": MyMemoryEngine, + "google": GoogleEngine, + "baidu": BaiduEngine, + "microsoft": MicrosoftEngine, +} + + +class Session: + """State shared by one run: engine choice, pacing, cache and entry limit.""" + + def __init__( + self, + delay: float = REQUEST_DELAY, + limit: int = 0, + proxy: str | None = None, + engine: str = ENGINE, + ): + """Initialize the run state. + + Args: + delay (float): Minimum seconds between two requests. + limit (int): Maximum entries translated per file; 0 means no limit. + proxy (str | None): Explicit proxy URL overriding ``HTTP(S)_PROXY``; None keeps + the environment setting. + engine (str): One of ``ENGINES``. + """ + self.throttle = Throttle(delay) + self.limit = limit + self.proxy = proxy + self.engine = engine + self.cache = {} # (target_code, msgid) -> translation, shared by every file + + +def is_rate_limited(error: Exception) -> bool: + """Report whether an exception looks like throttling rather than a bad request. + + Args: + error (Exception): Exception raised by the translation request. + + Returns: + bool: True when the message matches a known rate-limit marker. + """ + text = str(error).lower() + return any(marker in text for marker in RATE_LIMIT_MARKERS) + + +def proxy_map(proxy: str | None): + """Build the ``requests`` proxies mapping for an explicit proxy URL. + + Args: + proxy (str | None): Proxy URL, for example "http://127.0.0.1:7897". + + Returns: + dict | None: ``{"http": ..., "https": ...}``, or None to let requests use the + ``HTTP(S)_PROXY`` environment. + """ + if not proxy: + return None + return {"http": proxy, "https": proxy} + + +def translate_text(translator, text: str, session: "Session"): + """Translate one string, retrying according to the kind of failure. + + Args: + translator (object): Engine instance exposing ``translate(text)``. + text (str): Text to translate (a ``msgid``). + session (Session): Run state holding the request pacer. + + Returns: + str | None: The translation, or None when the entry failed and must stay + untranslated for a later run. + """ + for attempt in range(MAX_RETRIES): + try: + session.throttle.wait() + return translator.translate(text) + except Exception as e: + if attempt == MAX_RETRIES - 1: + print(f" ❌ 翻译失败: {text[:40]}... → {e}") + return None + if is_rate_limited(e): + wait = RATE_LIMIT_BACKOFF[min(attempt, len(RATE_LIMIT_BACKOFF) - 1)] + print(f" ⏸️ 被限流,等待 {wait}s 后重试 ({attempt + 1}/{MAX_RETRIES})") + time.sleep(wait) + else: + print(f" ⚠️ 重试 {attempt + 1}/{MAX_RETRIES}: {text[:30]}...") + time.sleep(2) + return None + + +def find_locale_dir(start_path: Path): + """Look for the ``locale/`` directory at or above ``start_path``. + + Args: + start_path (Path): Directory to start from, normally the script directory. + + Returns: + Path | None: The ``locale/`` directory, or None when none is found within + ten levels. + """ current = start_path.resolve() for _ in range(10): candidate = current / "locale" @@ -43,93 +502,263 @@ def find_locale_dir(start_path: Path) -> Path: return None -def translate_po_file(po_path: Path, target_lang: str, locale_dir: Path): - """翻译单个 .po 文件,带进度显示""" - rel_path = po_path.relative_to(locale_dir) - print(f"\n 📄 {rel_path}") +def is_generated_page(po_path: Path, lang_dir: Path) -> bool: + """Report whether a .po file belongs to a generated API page. + + Args: + po_path (Path): The catalogue to test. + lang_dir (Path): ``LC_MESSAGES`` directory it lives under. + + Returns: + bool: True for ``LC_MESSAGES/api/*.po``, whose entries come from docstrings. + """ + parts = po_path.relative_to(lang_dir).parts + return bool(parts) and parts[0] == "api" + + +def translate_po_file(po_path: Path, target_lang: str, locale_dir: Path, session: Session): + """Translate the untranslated entries of one .po file. + + Only entries with an empty ``msgstr`` and a non-empty ``msgid`` are touched; + failed ones stay empty, so the function can be run again until all are done. + + Args: + po_path (Path): The .po file to translate. + target_lang (str): Target language code, for example "ja" or "zh_CN". + locale_dir (Path): ``locale/`` root, used to print a relative path. + session (Session): Run state holding the engine, pacer, cache and limit. + + Returns: + tuple: ``(failed, translated)`` entry counts for this file. + + Raises: + RateLimitAbort: If ``MAX_CONSECUTIVE_FAILURES`` entries fail in a row. + """ + print(f"\n 📄 {po_path.relative_to(locale_dir)}") po = polib.pofile(str(po_path)) - target_code = LANG_MAP.get(target_lang, target_lang) + target_code = GOOGLE_LANG.get(target_lang, target_lang) empty_entries = [e for e in po if e.msgstr == "" and e.msgid] total = len(empty_entries) + if session.limit: + empty_entries = empty_entries[: session.limit] - if total == 0: + if not empty_entries: print(" ✅ 无需翻译(所有条目已有译文)") - return + return 0, 0 - print(f" 📝 待翻译: {total} 条") + suffix = f",本次处理前 {len(empty_entries)} 条" if session.limit else "" + print(f" 📝 待翻译: {total} 条{suffix}") + translator = ENGINES[session.engine](target_lang, session.proxy) translated = 0 + reused = 0 + failed = 0 + consecutive = 0 for idx, entry in enumerate(empty_entries, 1): - # 每 5 条或每 50 条显示一次进度 - if idx % 5 == 0 or idx == 1 or idx == total: - print(f" ⏳ 进度: {idx}/{total} ({idx * 100 // total}%) - {entry.msgid[:40]}...") - - # 带重试的翻译 - for attempt in range(MAX_RETRIES): - try: - translator = GoogleTranslator( - source=SOURCE_LANG, target=target_code, timeout=REQUEST_TIMEOUT - ) - entry.msgstr = translator.translate(entry.msgid) - translated += 1 - time.sleep(REQUEST_DELAY) - break # 成功则跳出重试循环 - except Exception as e: - if attempt < MAX_RETRIES - 1: - print(f" ⚠️ 重试 {attempt + 1}/{MAX_RETRIES}: {entry.msgid[:30]}...") - time.sleep(2) # 重试前等待 2 秒 - else: - print(f" ❌ 翻译失败: {entry.msgid[:40]}... → {e}") - continue - - if translated: + if idx % 10 == 0 or idx == 1 or idx == len(empty_entries): + print(f" ⏳ 进度: {idx}/{len(empty_entries)} - {entry.msgid[:40]}...") + + key = (target_code, entry.msgid) + if key in session.cache: # 同一字符串在多个文件/语言里重复出现,只请求一次 + entry.msgstr = session.cache[key] + reused += 1 + continue + + result = translate_text(translator, entry.msgid, session) + if result is None: + failed += 1 + consecutive += 1 + if consecutive >= MAX_CONSECUTIVE_FAILURES: + raise RateLimitAbort(f"{consecutive} consecutive failures") + continue + consecutive = 0 + entry.msgstr = result + session.cache[key] = result + translated += 1 + + if translated or reused: backup = po_path.with_suffix(po_path.suffix + ".bak") po_path.rename(backup) po.save(str(po_path)) - print(f" ✅ 完成: {translated}/{total} 条,备份: {backup.name}") + print(f" ✅ 完成: 新译 {translated} 条,复用 {reused} 条,备份: {backup.name}") else: print(" ⚠️ 未新增任何翻译") + if failed: + print(f" ℹ️ {failed} 条失败(保持未翻译),重新运行本脚本即可继续") + return failed, translated + + +def check_proxy(proxy: str | None) -> bool: + """Report whether an explicit proxy URL can be used, announcing it when it can. + + Args: + proxy (str | None): Proxy URL given on the command line; None means "use the + HTTP(S)_PROXY environment". + + Returns: + bool: True when the run may continue; False when the proxy is unusable, in + which case the reason has already been printed. + """ + if not proxy: + return True + if urlparse(proxy).scheme.startswith("socks") and find_spec("socks") is None: + print("❌ socks 代理需要 PySocks:uv add --group dev pysocks(或改用 http:// 代理)") + return False + print(f"🌐 使用代理: {proxy}") + return True + + +def collect_po_files(lang_dir: Path, include_generated: bool): + """List the catalogues to translate for one language. + + Args: + lang_dir (Path): ``LC_MESSAGES`` directory of the language. + include_generated (bool): Whether the generated ``api/*.po`` pages are included. + + Returns: + tuple: ``(po_files, skipped)`` — the catalogues to process and how many + generated pages were left out. + """ + po_files = sorted(lang_dir.rglob("*.po")) + if include_generated: + return po_files, 0 + kept = [p for p in po_files if not is_generated_page(p, lang_dir)] + return kept, len(po_files) - len(kept) + +def build_parser() -> argparse.ArgumentParser: + """Build the command-line parser. -def main(): - script_dir = Path(__file__).parent - locale_dir = find_locale_dir(script_dir) + Returns: + argparse.ArgumentParser: Parser with the ``--lang``, ``--engine``, ``--limit``, + ``--delay``, ``--proxy`` and ``--include-generated`` options. + """ + parser = argparse.ArgumentParser(description="批量翻译 locale/**/LC_MESSAGES 下的 .po 文件") + parser.add_argument( + "--lang", action="append", choices=LANGUAGES, help="只处理指定语言(可重复)" + ) + parser.add_argument( + "--engine", + choices=sorted(ENGINES), + default=ENGINE, + help="翻译引擎:mymemory(默认,免 key,无需代理)/ baidu(需 key)/" + " microsoft(需 key)/ google(常被反滥用拦截)", + ) + parser.add_argument("--limit", type=int, default=0, help="每个文件最多翻译多少条(0=全部)") + parser.add_argument( + "--delay", + type=float, + default=REQUEST_DELAY, + help=f"两次请求的最小间隔秒数(默认 {REQUEST_DELAY})", + ) + parser.add_argument( + "--proxy", + default=None, + help="翻译请求走的代理,如 http://127.0.0.1:7897(默认沿用 HTTP(S)_PROXY 环境变量)", + ) + parser.add_argument( + "--include-generated", + action="store_true", + help="连带翻译生成的 api/*.po(默认跳过:docstring 属 API 单源,且字符量占绝大部分)", + ) + return parser + + +def translate_language(lang: str, locale_dir: Path, session: Session, include_generated: bool): + """Translate every catalogue of one language. + + Args: + lang (str): Language code such as "ja" or "zh_CN". + locale_dir (Path): ``locale/`` root. + session (Session): Run state holding the engine, pacer, cache and limit. + include_generated (bool): Whether the generated ``api/*.po`` pages are included. + + Returns: + tuple: ``(failed, aborted)`` — the number of entries that failed and whether the + endpoint kept refusing, in which case the caller stops the run. + """ + lang_dir = locale_dir / lang / "LC_MESSAGES" + if not lang_dir.is_dir(): + print(f"⚠️ 跳过 {lang}:目录不存在") + return 0, False + + po_files, skipped = collect_po_files(lang_dir, include_generated) + if not po_files: + print(f"⚠️ 跳过 {lang}:没有可翻译的 .po 文件") + return 0, False + + suffix = f",跳过 {skipped} 个生成页" if skipped else "" + print(f"\n🌐 处理语言: {lang} ({len(po_files)} 个文件{suffix})") + + failed = 0 + for po_file in po_files: + try: + file_failed, _ = translate_po_file(po_file, lang, locale_dir, session) + except RateLimitAbort: + print(f"\n⛔ 连续 {MAX_CONSECUTIVE_FAILURES} 条翻译失败,停止本轮。") + print("ℹ️ google 引擎遇到的是反滥用拦截,换代理节点无效(实测跨大洲换 IP 仍 429)。") + print(" 改用 --engine baidu(国内直连)或 --engine azure(带 key)。") + print("ℹ️ 已完成的译文均已写入 .po;重新运行本脚本即可续跑。") + return failed, True + failed += file_failed + return failed, False + + +def main(argv=None): + """Translate the configured languages under ``locale/``. + + Args: + argv (list | None): Command-line arguments; None uses ``sys.argv``. + + Returns: + int: 0 on completion (individual entries may still have failed), 1 when + ``locale/`` is missing, the engine is unconfigured, or the run stopped on + repeated failures. + """ + args = build_parser().parse_args(argv) + locale_dir = find_locale_dir(Path(__file__).parent) if not locale_dir: print("❌ 未找到 locale/ 目录") - sys.exit(1) + return 1 print(f"✅ 找到 locale 目录: {locale_dir}") + print(f"🔧 引擎: {args.engine}") if not ENABLE_TRANSLATION: print("ℹ️ 翻译功能已关闭,仅扫描文件...") for lang in LANGUAGES: lang_path = locale_dir / lang / "LC_MESSAGES" if lang_path.exists(): - po_files = list(lang_path.rglob("*.po")) - print(f" {lang}: {len(po_files)} 个 .po 文件") - return - - for lang in LANGUAGES: - lang_dir = locale_dir / lang / "LC_MESSAGES" - if not lang_dir.is_dir(): - print(f"⚠️ 跳过 {lang}:目录不存在") - continue + print(f" {lang}: {len(list(lang_path.rglob('*.po')))} 个 .po 文件") + return 0 - po_files = list(lang_dir.rglob("*.po")) - if not po_files: - print(f"⚠️ 跳过 {lang}:没有 .po 文件") - continue + if not check_proxy(args.proxy): + return 1 + + try: # fail fast on missing credentials instead of failing entry by entry + ENGINES[args.engine](LANGUAGES[0], args.proxy) + except ConfigError as e: + print(f"❌ {e}") + return 1 - print(f"\n🌐 处理语言: {lang} ({len(po_files)} 个文件)") - for po_file in po_files: - translate_po_file(po_file, lang, locale_dir) + session = Session(args.delay, args.limit, args.proxy, args.engine) + total_failed = 0 + for lang in args.lang or LANGUAGES: + failed, aborted = translate_language(lang, locale_dir, session, args.include_generated) + total_failed += failed + if aborted: + return 1 - print("\n✅ 所有翻译任务完成!") + if total_failed: + print(f"\n⚠️ 完成,但有 {total_failed} 条失败(保持未翻译),重新运行本脚本即可继续。") + else: + print("\n✅ 所有翻译任务完成!") print("📌 请运行: sphinx-intl build") + return 0 if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/docs/conf.py b/docs/conf.py index 98125b6..81eca48 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,13 @@ +"""Sphinx configuration for the PyFlow documentation. + +Docstrings are read as Google style by ``sphinx.ext.napoleon`` and pulled into the pages by +``sphinx.ext.autodoc``; the API pages under ``docs/api/`` are generated with ``sphinx-apidoc``. +The rules they follow live in ``docs/DOCSTRING_GUIDE.md``. +""" + +import os +import sys + # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -14,7 +24,29 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [] +# autodoc imports the package, so the repository root (one level up) must be on sys.path. +# Without this the docs only build from an environment where PyFlow is installed; reBuild.sh +# runs from docs/, where Sphinx puts only docs/ itself on sys.path. +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", +] + +# Docstrings follow the Google style (docs/DOCSTRING_GUIDE.md); the NumPy style is rejected +# there, so leave its parser off instead of silently accepting both. +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +# The API pages under docs/api are generated with sphinx-apidoc; these defaults give them the +# shape docs/DOCSTRING_GUIDE.md requires (class __init__ documented, source order). +autodoc_default_options = { + "members": True, + "show-inheritance": True, + "member-order": "bysource", + "special-members": "__init__", +} locale_dirs = ["locale/"] templates_path = ["_templates"] diff --git a/docs/index.rst b/docs/index.rst index b7fa068..ccd3aec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,4 +21,5 @@ documentation for details. File_Transfer/File_Transfer Port_Allocation/Port_Allocation Instance_Setup/Instance_Setup + api/index Crypto/Crypto diff --git a/docs/locale/ja/LC_MESSAGES/File_Transfer/File_Transfer.po b/docs/locale/ja/LC_MESSAGES/File_Transfer/File_Transfer.po index 94fcb3f..dc92004 100644 --- a/docs/locale/ja/LC_MESSAGES/File_Transfer/File_Transfer.po +++ b/docs/locale/ja/LC_MESSAGES/File_Transfer/File_Transfer.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 13:19+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: ja \n" @@ -691,11 +691,17 @@ msgid "Commands (client console only; rejected on the server console):" msgstr "クライアント コンソール (受信者はサーバー):" #: ../../File_Transfer/File_Transfer.rst:413 14406316555743359a854540ab6937c8 -msgid "``/forward_file ... ...``" +#, fuzzy +msgid "" +"``/forward_file ... ... " +"[destination_file_path]``" msgstr "``/forward_file ... ...``" #: ../../File_Transfer/File_Transfer.rst:414 4dc576898177404da124649f7ce9ac28 -msgid "``/forward_folder ... ...``" +#, fuzzy +msgid "" +"``/forward_folder ... ... " +"[destination_file_path]``" msgstr "``/forward_folder <フォルダ 1> <フォルダ 2> ... ...``" #: ../../File_Transfer/File_Transfer.rst:416 220dfbf0a0d84559a8befb4eee500ba0 @@ -709,11 +715,25 @@ msgstr "" "(サーバーに接続されていない) ターゲット アドレス、またはサーバー自体に等しいターゲット " "アドレスはスキップされ、残りのターゲットは引き続き処理されます。" -#: ../../File_Transfer/File_Transfer.rst:424 f6697695e9ef4e3694fa9046e41578e0 +#: ../../File_Transfer/File_Transfer.rst:424 6137bf9406784c6f82d9f41560a52566 +msgid "" +"Like every transfer family, both commands accept an optional trailing " +"``destination_file_path`` that replaces the default save directory on every " +"receiving client: a forwarded file lands at ``/`` and" +" a forwarded folder keeps its structure under " +"``//...``. When the argument is omitted the " +"targets write to their default ``file_transfer_dir``." +msgstr "" +"すべての転送ファミリと同様に、両方のコマンドは、すべての受信クライアントのデフォルトの保存ディレクトリを置き換えるオプションの末尾の''" +" destination_file_path " +"''を受け入れます。転送されたファイルは''/''に着地し、転送されたフォルダはその構造を''//..." +" ''の下に保持します。引数が省略されると、ターゲットはデフォルトの'' file_transfer_dir ''に書き込みます。" + +#: ../../File_Transfer/File_Transfer.rst:435 f6697695e9ef4e3694fa9046e41578e0 msgid "The data path reuses the protocol's own transfer machinery:" msgstr "データ パスは、プロトコル独自の転送機構を再利用します。" -#: ../../File_Transfer/File_Transfer.rst:427 6ee037f0e4024d2988fcb2ccf3deef70 +#: ../../File_Transfer/File_Transfer.rst:438 6ee037f0e4024d2988fcb2ccf3deef70 msgid "" "The forwarding client streams the file with the standard file-transfer byte " "stream (metadata header + 64 KiB chunks) to a transfer socket on the server." @@ -721,7 +741,7 @@ msgstr "" "転送クライアントは、標準のファイル転送バイト ストリーム (メタデータ ヘッダー + 64 KiB チャンク) " "を使用してファイルをサーバー上の転送ソケットにストリーミングします。" -#: ../../File_Transfer/File_Transfer.rst:432 43c94e746a5c46748846d0dc18a4e0ff +#: ../../File_Transfer/File_Transfer.rst:443 43c94e746a5c46748846d0dc18a4e0ff msgid "" "The server acts as a pure relay: it reads the stream into per-target memory " "queues and writes each chunk to every target's transfer socket. The server " @@ -732,7 +752,7 @@ msgstr "" "キューに読み取り、各チャンクをすべてのターゲットの転送ソケットに書き込みます。サーバーは、サイズ " "ヘッダーを超えてファイルの内容を解析したり、ディスクに書き込んだりすることはありません。" -#: ../../File_Transfer/File_Transfer.rst:439 9dec57916af34e109bee7ce1e0bae2d4 +#: ../../File_Transfer/File_Transfer.rst:450 9dec57916af34e109bee7ce1e0bae2d4 msgid "" "Every target client receives the stream with the ordinary receive path " "(``file_transfer_mode_recv``) and writes it to its own local disk, exactly " @@ -741,11 +761,11 @@ msgstr "" "すべてのターゲットクライアントは、通常の受信パス (``file_transfer_mode_recv``) " "でストリームを受信し、サーバーがファイルを直接プッシュしたかのように、それを独自のローカルディスクに書き込みます。" -#: ../../File_Transfer/File_Transfer.rst:445 a095abaf821e41a6a66adbb776bc35ef +#: ../../File_Transfer/File_Transfer.rst:456 a095abaf821e41a6a66adbb776bc35ef msgid "### Memory Bounding and Flow Control" msgstr "### メモリ境界とフロー制御" -#: ../../File_Transfer/File_Transfer.rst:447 244214dd675345f8a72b1088ad99fea0 +#: ../../File_Transfer/File_Transfer.rst:458 244214dd675345f8a72b1088ad99fea0 msgid "" "Because uploader, server and targets may have different bandwidths, data can" " pile up in the server's memory. Both ``TCP_Server_Base`` and " @@ -770,25 +790,25 @@ msgstr "" " ``/pause_trans``/``/start_trans`` " "ハンドラは両側に存在するため、どちらの側でもデータをバッファリングするときに転送を抑制できます。" -#: ../../File_Transfer/File_Transfer.rst:472 0aa14deaf6474437b421a65e21fce7d8 +#: ../../File_Transfer/File_Transfer.rst:483 0aa14deaf6474437b421a65e21fce7d8 msgid "Concurrency and Threading" msgstr "同時実行性とスレッド化" -#: ../../File_Transfer/File_Transfer.rst:474 825c5e1a8b9b405c967ca49a1861e258 +#: ../../File_Transfer/File_Transfer.rst:485 825c5e1a8b9b405c967ca49a1861e258 msgid "" "Both the server and the client use multiple levels of concurrency control to" " ensure stability during file transfers." msgstr "サーバーとクライアントの両方は、ファイル転送中の安定性を確保するために複数レベルの同時実行制御を使用します。" -#: ../../File_Transfer/File_Transfer.rst:478 0d48b1245cd54597928cfc28d5b3c248 +#: ../../File_Transfer/File_Transfer.rst:489 0d48b1245cd54597928cfc28d5b3c248 msgid "### File Transfer Semaphore" msgstr "### ファイル転送セマフォ" -#: ../../File_Transfer/File_Transfer.rst:480 e2674ee9e0584513bc53aa815603fd24 +#: ../../File_Transfer/File_Transfer.rst:491 e2674ee9e0584513bc53aa815603fd24 msgid "Client: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" msgstr "クライアント: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:482 1c0d27d13ad844159d29c6e662d51d09 +#: ../../File_Transfer/File_Transfer.rst:493 1c0d27d13ad844159d29c6e662d51d09 msgid "" "Server: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" @@ -796,7 +816,7 @@ msgstr "" "サーバー: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:485 5b0c8289b2c9460cb7305101226b66a7 +#: ../../File_Transfer/File_Transfer.rst:496 5b0c8289b2c9460cb7305101226b66a7 msgid "" "This semaphore limits the number of simultaneous file transfers (used " "primarily when sending folders or multiple files). Each transfer runs in its" @@ -805,11 +825,11 @@ msgstr "" "このセマフォは、同時ファイル転送の数を制限します " "(主にフォルダーまたは複数のファイルを送信する場合に使用されます)。各転送は独自のスレッドで実行され、セマフォはスレッドが開始される前に取得されます。" -#: ../../File_Transfer/File_Transfer.rst:492 6d93783b298e4ce98b999ff890d4a5f4 +#: ../../File_Transfer/File_Transfer.rst:503 6d93783b298e4ce98b999ff890d4a5f4 msgid "### Threading Model" msgstr "### スレッドモデル" -#: ../../File_Transfer/File_Transfer.rst:494 036a972600e645119d8a523fa08a52db +#: ../../File_Transfer/File_Transfer.rst:505 036a972600e645119d8a523fa08a52db msgid "" "Each file transfer runs in a dedicated daemon thread, created by the " "``_thread`` wrapper functions (e.g., " @@ -820,7 +840,7 @@ msgstr "" "``file_transfer_client_recv_client_start_thread``) " "によって作成された専用のデーモンスレッドで実行されます。これにより、遅い転送によってメイン制御ループがブロックされるのを防ぎます。" -#: ../../File_Transfer/File_Transfer.rst:500 a84aaf5f9fb64d92b97c32185a1e7cb5 +#: ../../File_Transfer/File_Transfer.rst:511 a84aaf5f9fb64d92b97c32185a1e7cb5 msgid "" "The thread that receives the transfer command (e.g., the server's " "``handle_command`` thread) does not wait for the transfer to complete; it " @@ -829,7 +849,7 @@ msgstr "" "転送コマンドを受信するスレッド (例: サーバーの ``handle_command`` スレッド) は、転送が完了するのを待ちません。ワーカー " "スレッドを生成した直後に戻ります。" -#: ../../File_Transfer/File_Transfer.rst:505 669fb6da8e7444dd829a6e14295648b1 +#: ../../File_Transfer/File_Transfer.rst:516 669fb6da8e7444dd829a6e14295648b1 msgid "" "The low-level receive function (``file_transfer_mode_recv``) blocks while " "reading from the transfer socket, but because it runs in a dedicated thread," @@ -838,11 +858,11 @@ msgstr "" "低レベルの受信関数 (``file_transfer_mode_recv``) " "は、転送ソケットからの読み取り中にブロックされますが、専用スレッドで実行されるため、メイン接続は応答したままになります。" -#: ../../File_Transfer/File_Transfer.rst:511 e39f8ae06ae84a6b84de8a3008ca17cb +#: ../../File_Transfer/File_Transfer.rst:522 e39f8ae06ae84a6b84de8a3008ca17cb msgid "### Thread Pool for Custom Commands" msgstr "### カスタム コマンド用のスレッド プール" -#: ../../File_Transfer/File_Transfer.rst:513 3ad49853529d4563bacdfb4ca6eee115 +#: ../../File_Transfer/File_Transfer.rst:524 3ad49853529d4563bacdfb4ca6eee115 msgid "" "Both classes also provide a ``ThreadPoolExecutor`` " "(``self._custom_executor``) for custom command handlers. When a handler is " @@ -857,11 +877,11 @@ msgstr "" "``max_custom_workers`` " "に制限します。このメカニズムはファイル転送セマフォから**独立**しており、汎用コマンド処理を目的としています。" -#: ../../File_Transfer/File_Transfer.rst:528 1862d74da28a40219f82cfd7af89c126 +#: ../../File_Transfer/File_Transfer.rst:539 1862d74da28a40219f82cfd7af89c126 msgid "Port Allocation and Management" msgstr "ポートの割り当てと管理" -#: ../../File_Transfer/File_Transfer.rst:530 bd53ec8aa4ef498aa0815db16ed03140 +#: ../../File_Transfer/File_Transfer.rst:541 bd53ec8aa4ef498aa0815db16ed03140 msgid "" "File transfers require ephemeral ports for the secondary data connections. " "The ``palloc()`` and ``pfree()`` methods are used to obtain and release " @@ -870,7 +890,7 @@ msgstr "" "ファイル転送には、セカンダリ データ接続用の一時ポートが必要です。 ``palloc()`` メソッドと ``pfree()`` " "メソッドは、これらのポートを取得および解放するために使用されます。次の 2 つのモードが利用可能です。" -#: ../../File_Transfer/File_Transfer.rst:536 eeaa9379d7d941159ac34f30ff6bb5f9 +#: ../../File_Transfer/File_Transfer.rst:547 eeaa9379d7d941159ac34f30ff6bb5f9 msgid "" "**Automatic mode** (``is_hand_alloc_port=False``): ``palloc()`` returns " "``0``, and the operating system assigns a free port when the socket is " @@ -879,7 +899,7 @@ msgstr "" "**自動モード** (``is_hand_alloc_port=False``): ``palloc()`` は ``0`` " "を返し、ソケットがバインドされているときにオペレーティング システムが空きポートを割り当てます。これは、ほとんどの使用例で推奨されるモードです。" -#: ../../File_Transfer/File_Transfer.rst:541 80e08ac6614b4dcd88cd0e4d31a3da64 +#: ../../File_Transfer/File_Transfer.rst:552 80e08ac6614b4dcd88cd0e4d31a3da64 msgid "" "**Manual mode** (``is_hand_alloc_port=True``): Ports are drawn from a " "configurable range ``[self.min_port, self.max_port]`` with a step size " @@ -891,7 +911,7 @@ msgstr "" "self.max_port]`` からステップ サイズ ``port_add_step`` で描画されます。サーバーは許可された範囲を " "/client_alloc_port_range 経由でクライアントにブロードキャストし、クライアントは同じ手動割り当てロジックを使用します。" -#: ../../File_Transfer/File_Transfer.rst:551 29cfdab183eb4f9f83de41ef62a60785 +#: ../../File_Transfer/File_Transfer.rst:562 29cfdab183eb4f9f83de41ef62a60785 msgid "" "*Note: For more details about port allocation, please visit the Port " "Allocation API sections in :doc:`TCP_Server_APIs` and " @@ -900,15 +920,15 @@ msgstr "" "*注意: ポート割り当ての詳細については、:doc:`TCP_Server_APIs` および :doc:`TCP_Client_APIs` " "のポート割り当て API セクションを参照してください。*" -#: ../../File_Transfer/File_Transfer.rst:559 189736d8d1f8464b8d301f219700fda4 +#: ../../File_Transfer/File_Transfer.rst:570 189736d8d1f8464b8d301f219700fda4 msgid "Error Handling and Timeouts" msgstr "エラー処理とタイムアウト" -#: ../../File_Transfer/File_Transfer.rst:561 bcc373777989436f9df3657446d1a7f2 +#: ../../File_Transfer/File_Transfer.rst:572 bcc373777989436f9df3657446d1a7f2 msgid "### Timeout Values" msgstr "### タイムアウト値" -#: ../../File_Transfer/File_Transfer.rst:563 27d75d097a804ec28ebec5c67469c9c6 +#: ../../File_Transfer/File_Transfer.rst:574 27d75d097a804ec28ebec5c67469c9c6 msgid "" "**Start signal timeout**: 10 seconds. If the receiver does not send " "``server_start_file_transfer_sign`` within this time, the sender aborts." @@ -916,7 +936,7 @@ msgstr "" "**開始信号のタイムアウト**: 10 秒。受信者がこの時間内に ``server_start_file_transfer_sign`` " "を送信しない場合、送信者は中止します。" -#: ../../File_Transfer/File_Transfer.rst:567 93e877c411eb4c96bd7861ff61f35e7a +#: ../../File_Transfer/File_Transfer.rst:578 93e877c411eb4c96bd7861ff61f35e7a msgid "" "**Port negotiation timeout**: 20 seconds. The initiator waits for the peer's" " ``/server_file_transfer_port`` response." @@ -924,7 +944,7 @@ msgstr "" "**ポート ネゴシエーション タイムアウト**: 20 秒。イニシエータはピアの ``/server_file_transfer_port`` " "応答を待ちます。" -#: ../../File_Transfer/File_Transfer.rst:570 9e1d395bb59540a49393bebe6630e5eb +#: ../../File_Transfer/File_Transfer.rst:581 9e1d395bb59540a49393bebe6630e5eb msgid "" "**Completion acknowledgement timeout**: ``30 + (file_size // (100 * 1024 * " "1024)) * 10`` seconds. Larger files get proportionally more time." @@ -932,33 +952,33 @@ msgstr "" "**完了確認タイムアウト**: ``30 + (file_size // (100 * 1024 * 1024)) * 10`` " "秒。ファイルが大きいほど、それに比例して時間がかかります。" -#: ../../File_Transfer/File_Transfer.rst:574 6b4e6135669b41178fdbb47ea5975381 +#: ../../File_Transfer/File_Transfer.rst:585 6b4e6135669b41178fdbb47ea5975381 msgid "### Error Signalling" msgstr "### エラー通知" -#: ../../File_Transfer/File_Transfer.rst:576 448fdc6ba3dd4d9eae925e7e241f8cb8 +#: ../../File_Transfer/File_Transfer.rst:587 448fdc6ba3dd4d9eae925e7e241f8cb8 msgid "" "Any error during the handshake or data transfer causes the failing side to " "send ``error_sign`` over the transfer socket." msgstr "ハンドシェイクまたはデータ転送中にエラーが発生すると、失敗した側が転送ソケット経由で ``error_sign`` を送信します。" -#: ../../File_Transfer/File_Transfer.rst:579 5b4c23a93e984ce0af2e43cf4752225b +#: ../../File_Transfer/File_Transfer.rst:590 5b4c23a93e984ce0af2e43cf4752225b msgid "" "The other side, upon receiving the error sign, closes the transfer socket " "and aborts the transfer." msgstr "相手側はエラーサインを受信すると、転送ソケットを閉じて転送を中止します。" -#: ../../File_Transfer/File_Transfer.rst:582 b337a2726ded4d619c5e8026bef3f6ea +#: ../../File_Transfer/File_Transfer.rst:593 b337a2726ded4d619c5e8026bef3f6ea msgid "" "The main control connection remains unaffected; only the transfer socket is " "closed." msgstr "メイン制御接続は影響を受けません。転送ソケットのみが閉じられます。" -#: ../../File_Transfer/File_Transfer.rst:586 68be52c8e5ce447a9c5ec51a661229cf +#: ../../File_Transfer/File_Transfer.rst:597 68be52c8e5ce447a9c5ec51a661229cf msgid "### Exception Handling" msgstr "### 例外処理" -#: ../../File_Transfer/File_Transfer.rst:588 036db9bc1858417ca589816028b83f80 +#: ../../File_Transfer/File_Transfer.rst:599 036db9bc1858417ca589816028b83f80 msgid "" "All socket operations are wrapped in try-except blocks. When an exception " "occurs (e.g., connection reset, file not found), the error is logged with " @@ -969,11 +989,11 @@ msgstr "" "``traceback.print_exc()`` でログに記録され、転送は正常に中止されます。可能な場合は ``error_sign`` " "が送信され、転送ソケットが閉じられます。" -#: ../../File_Transfer/File_Transfer.rst:600 fe9377db9cf04b9da0dbe4d07c730adf +#: ../../File_Transfer/File_Transfer.rst:611 fe9377db9cf04b9da0dbe4d07c730adf msgid "Related API Definitions" msgstr "関連する API 定義" -#: ../../File_Transfer/File_Transfer.rst:602 cc71cb9b1d6642a2acc89d45a49022cc +#: ../../File_Transfer/File_Transfer.rst:613 cc71cb9b1d6642a2acc89d45a49022cc msgid "" "This section lists all public file-transfer related methods in " "``TCP_Server_Base`` and ``TCP_Client_Base``. For a complete list of all " @@ -982,11 +1002,11 @@ msgstr "" "このセクションでは、 ``TCP_Server_Base`` および ``TCP_Client_Base`` 内のすべてのパブリック " "ファイル転送関連メソッドをリストします。すべてのパブリック API の完全なリストについては、このドキュメントの最後にある表を参照してください。" -#: ../../File_Transfer/File_Transfer.rst:608 d6683e415f794c5bb693f8c24370e7f9 +#: ../../File_Transfer/File_Transfer.rst:619 d6683e415f794c5bb693f8c24370e7f9 msgid "### Server-Side File Transfer APIs" msgstr "### サーバー側ファイル転送 API" -#: ../../File_Transfer/File_Transfer.rst:618 9123c69197c34d93bb68d088897ebeca +#: ../../File_Transfer/File_Transfer.rst:629 9123c69197c34d93bb68d088897ebeca msgid "" "Initiates a server-to-client file transfer. ``message`` is the command " "string (e.g., ``/file /path/to/file.txt (127.0.0.1,54321)``). If " @@ -997,11 +1017,11 @@ msgstr "" "/path/to/file.txt (127.0.0.1,54321)``)。 ``file_folder_abspath`` が指定されている場合 " "(フォルダ転送用)、親フォルダの絶対パスを指定します。" -#: ../../File_Transfer/File_Transfer.rst:633 367dfd82c95c413d963a15152469fc44 +#: ../../File_Transfer/File_Transfer.rst:644 367dfd82c95c413d963a15152469fc44 msgid "Thread-safe version that starts a new thread for the transfer." msgstr "転送用に新しいスレッドを開始するスレッドセーフ バージョン。" -#: ../../File_Transfer/File_Transfer.rst:642 d4aa76b7eb2c46c29adee0120a939b66 +#: ../../File_Transfer/File_Transfer.rst:653 d4aa76b7eb2c46c29adee0120a939b66 msgid "" "Sends an entire folder from server to client. ``message`` should be of the " "form ``/file_folder ``." @@ -1009,7 +1029,7 @@ msgstr "" "フォルダー全体をサーバーからクライアントに送信します。 ``message`` は ``/file_folder " " `` の形式にする必要があります。" -#: ../../File_Transfer/File_Transfer.rst:652 fcb785ff144746fab81e95ec2ab056e1 +#: ../../File_Transfer/File_Transfer.rst:663 fcb785ff144746fab81e95ec2ab056e1 msgid "" "Sends multiple files to multiple clients. The message format is " "``/multiple_file_multiple_client ... " @@ -1018,7 +1038,7 @@ msgstr "" "複数のファイルを複数のクライアントに送信します。メッセージの形式は「/multiple_file_multiple_client " " ... ...」です。ファイルはクライアントの前に表示される必要があります。" -#: ../../File_Transfer/File_Transfer.rst:664 9c6f024528f344e399b62023c7f8c858 +#: ../../File_Transfer/File_Transfer.rst:675 9c6f024528f344e399b62023c7f8c858 msgid "" "Sends different file lists to different clients. The message alternates " "between groups: a list of files, then a list of client addresses, then the " @@ -1030,29 +1050,29 @@ msgstr "" "``/diff_multiple_file_diff_multiple_client a.txt b.txt (ip1,port1) " "(ip2,port2) c.txt (ip3,port3)``" -#: ../../File_Transfer/File_Transfer.rst:682 f8a880287b7b49d4bdc2239ecf4a0577 +#: ../../File_Transfer/File_Transfer.rst:693 f8a880287b7b49d4bdc2239ecf4a0577 msgid "" "Receives a file from a client. Called internally when the server receives a " "``/file`` command from a client." msgstr "クライアントからファイルを受信します。サーバーがクライアントから ``/file`` コマンドを受信したときに内部的に呼び出されます。" -#: ../../File_Transfer/File_Transfer.rst:698 97bfaa9c24ae47c39328707b8f17a91a +#: ../../File_Transfer/File_Transfer.rst:709 97bfaa9c24ae47c39328707b8f17a91a msgid "" "Low-level receive function that performs the handshake and writes the " "incoming file to disk." msgstr "ハンドシェイクを実行し、受信ファイルをディスクに書き込む低レベルの受信関数。" -#: ../../File_Transfer/File_Transfer.rst:711 db6a97ca42ba43e59d5c20695039d4ee +#: ../../File_Transfer/File_Transfer.rst:722 db6a97ca42ba43e59d5c20695039d4ee msgid "" "Low-level send function that connects to the receiver and transmits the " "file." msgstr "受信機に接続してファイルを送信する低レベルの送信機能。" -#: ../../File_Transfer/File_Transfer.rst:713 3e7b689215e840bebd368b4d29104ebc +#: ../../File_Transfer/File_Transfer.rst:724 3e7b689215e840bebd368b4d29104ebc msgid "### Client-Side File Transfer APIs" msgstr "### クライアント側のファイル転送 API" -#: ../../File_Transfer/File_Transfer.rst:723 1e97e492d6504579a9a265eb1242395e +#: ../../File_Transfer/File_Transfer.rst:734 1e97e492d6504579a9a265eb1242395e msgid "" "Initiates a client-to-server file transfer. ``message`` is the user command " "(e.g., ``/file mydoc.txt``). Used internally by the interactive console." @@ -1060,79 +1080,79 @@ msgstr "" "クライアントからサーバーへのファイル転送を開始します。 ``message`` はユーザーコマンドです (例: ``/file " "mydoc.txt``)。対話型コンソールによって内部的に使用されます。" -#: ../../File_Transfer/File_Transfer.rst:735 -#: ../../File_Transfer/File_Transfer.rst:786 0cd2695763114a0b831df0bfa80a3d56 +#: ../../File_Transfer/File_Transfer.rst:746 +#: ../../File_Transfer/File_Transfer.rst:797 0cd2695763114a0b831df0bfa80a3d56 msgid "Thread-safe version." msgstr "スレッドセーフなバージョン。" -#: ../../File_Transfer/File_Transfer.rst:744 261399ca508d463eafa7f03a00bfc658 +#: ../../File_Transfer/File_Transfer.rst:755 261399ca508d463eafa7f03a00bfc658 msgid "Sends a folder from client to server." msgstr "フォルダーをクライアントからサーバーに送信します。" -#: ../../File_Transfer/File_Transfer.rst:753 ef5e3b92b11c4530960c1c344a51c73b +#: ../../File_Transfer/File_Transfer.rst:764 ef5e3b92b11c4530960c1c344a51c73b msgid "Sends multiple files from client to server." msgstr "複数のファイルをクライアントからサーバーに送信します。" -#: ../../File_Transfer/File_Transfer.rst:762 f2c90ee949d7484480cbb2cd5310bf26 +#: ../../File_Transfer/File_Transfer.rst:773 f2c90ee949d7484480cbb2cd5310bf26 msgid "Sends multiple folders from client to server." msgstr "複数のフォルダーをクライアントからサーバーに送信します。" -#: ../../File_Transfer/File_Transfer.rst:775 0b13d26a40174243a15698b4bfcbb69f +#: ../../File_Transfer/File_Transfer.rst:786 0b13d26a40174243a15698b4bfcbb69f msgid "" "Receives a file from the server (called when the server initiates a " "transfer)." msgstr "サーバーからファイルを受信します (サーバーが転送を開始するときに呼び出されます)。" -#: ../../File_Transfer/File_Transfer.rst:797 3b78a4355d7f4ee2bbbe6bf934a962c0 +#: ../../File_Transfer/File_Transfer.rst:808 3b78a4355d7f4ee2bbbe6bf934a962c0 msgid "Receives a folder from the server." msgstr "サーバーからフォルダーを受信します。" -#: ../../File_Transfer/File_Transfer.rst:812 3d044e0754b94d1289b491502ce83610 +#: ../../File_Transfer/File_Transfer.rst:823 3d044e0754b94d1289b491502ce83610 msgid "Low-level receive function on the client side." msgstr "クライアント側の低レベル受信機能。" -#: ../../File_Transfer/File_Transfer.rst:824 7311023a7fa644ed9b57a2873cd3bca8 +#: ../../File_Transfer/File_Transfer.rst:835 7311023a7fa644ed9b57a2873cd3bca8 msgid "" "Low‑level send function on the client side (identical to server's version)." msgstr "クライアント側の低レベル送信機能 (サーバーのバージョンと同じ)。" -#: ../../File_Transfer/File_Transfer.rst:829 7e74e9a09a8a4cf2a8a372a50b1ee51b +#: ../../File_Transfer/File_Transfer.rst:840 7e74e9a09a8a4cf2a8a372a50b1ee51b msgid "Public API Summary" msgstr "パブリック API の概要" -#: ../../File_Transfer/File_Transfer.rst:831 87aaf0a5d3b44f41a419d97b6567f6d0 +#: ../../File_Transfer/File_Transfer.rst:842 87aaf0a5d3b44f41a419d97b6567f6d0 msgid "" "All public APIs (including non-file-transfer methods) are listed below for " "reference." msgstr "すべてのパブリック API (ファイル転送以外のメソッドを含む) を参考のために以下にリストします。" -#: ../../File_Transfer/File_Transfer.rst:835 a195ee393f2a442c810e59811a6ae126 +#: ../../File_Transfer/File_Transfer.rst:846 a195ee393f2a442c810e59811a6ae126 msgid "### TCP_Server_Base Public APIs" msgstr "### TCP_Server_Base パブリック API" -#: ../../File_Transfer/File_Transfer.rst:837 0408d74a9140472e9a774143a60e5749 +#: ../../File_Transfer/File_Transfer.rst:848 0408d74a9140472e9a774143a60e5749 msgid "``file_transfer_server_recv_client_start``" msgstr "``file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:838 24a33cb212844e04a8a271ada32412f0 +#: ../../File_Transfer/File_Transfer.rst:849 24a33cb212844e04a8a271ada32412f0 msgid "``file_transfer_server_recv_client_start_thread``" msgstr "``file_transfer_server_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:839 7e286d1ece6940868bea6b937486e617 +#: ../../File_Transfer/File_Transfer.rst:850 7e286d1ece6940868bea6b937486e617 msgid "``folder_file_transfer_server_recv_client_start``" msgstr "``folder_file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:840 37fc5bd346c84415bb15138a42508fbe +#: ../../File_Transfer/File_Transfer.rst:851 37fc5bd346c84415bb15138a42508fbe msgid "``multiple_file_multiple_client_transfer_server_recv_client_start``" msgstr "``multiple_file_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:841 38a2b16e3ab648688cf7e8d1cae8be72 +#: ../../File_Transfer/File_Transfer.rst:852 38a2b16e3ab648688cf7e8d1cae8be72 msgid "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" msgstr "" "「diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start」" -#: ../../File_Transfer/File_Transfer.rst:843 11476e0eaa3b4a4286612812e4e2c004 +#: ../../File_Transfer/File_Transfer.rst:854 11476e0eaa3b4a4286612812e4e2c004 msgid "" "(The low-level helpers ``file_transfer_server_recv_server_start``, " "``file_transfer_mode_recv``, and ``file_transfer_mode`` are not considered " @@ -1142,65 +1162,65 @@ msgstr "" "``file_transfer_server_recv_server_start``、``file_transfer_mode_recv``、および " "``file_transfer_mode`` はパブリックとはみなされませんが、完全を期すために文書化されています。)" -#: ../../File_Transfer/File_Transfer.rst:849 907e861cebe648fbacb799bae8bb15e0 +#: ../../File_Transfer/File_Transfer.rst:860 907e861cebe648fbacb799bae8bb15e0 msgid "### TCP_Client_Base Public APIs" msgstr "### TCP_Client_Base パブリック API" -#: ../../File_Transfer/File_Transfer.rst:851 88bd80083f754caeb026f9ce1b8c6b55 +#: ../../File_Transfer/File_Transfer.rst:862 88bd80083f754caeb026f9ce1b8c6b55 msgid "``file_transfer_client_recv_client_start``" msgstr "``file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:852 3b21ff73da694465906482412b4fb4e3 +#: ../../File_Transfer/File_Transfer.rst:863 3b21ff73da694465906482412b4fb4e3 msgid "``file_transfer_client_recv_client_start_thread``" msgstr "``file_transfer_client_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:853 c8af43896a2443a5b14bd29863731c2c +#: ../../File_Transfer/File_Transfer.rst:864 c8af43896a2443a5b14bd29863731c2c msgid "``folder_file_transfer_client_recv_client_start``" msgstr "``folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:854 7a3d3e4a52334b169a62d3b30d7a3190 +#: ../../File_Transfer/File_Transfer.rst:865 7a3d3e4a52334b169a62d3b30d7a3190 msgid "``multiple_file_transfer_client_recv_client_start``" msgstr "``multiple_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:855 19e18754c4674842ad5116f709588e03 +#: ../../File_Transfer/File_Transfer.rst:866 19e18754c4674842ad5116f709588e03 msgid "``multiple_folder_file_transfer_client_recv_client_start``" msgstr "``multiple_folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:856 4cf80f9f34e045c59a680d0450c9103a +#: ../../File_Transfer/File_Transfer.rst:867 4cf80f9f34e045c59a680d0450c9103a msgid "``file_transfer_client_recv_server_start``" msgstr "``file_transfer_client_recv_server_start``" -#: ../../File_Transfer/File_Transfer.rst:857 c09f5fcba03f461f89238dd31abf6e88 +#: ../../File_Transfer/File_Transfer.rst:868 c09f5fcba03f461f89238dd31abf6e88 msgid "``file_transfer_client_recv_server_start_thread``" msgstr "``file_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:858 ad39bc6b71f048b09bca3b895e9d32f8 +#: ../../File_Transfer/File_Transfer.rst:869 ad39bc6b71f048b09bca3b895e9d32f8 msgid "``file_folder_transfer_client_recv_server_start_thread``" msgstr "``file_folder_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:860 99140d0be72649199911a57a26e2f2cf +#: ../../File_Transfer/File_Transfer.rst:871 99140d0be72649199911a57a26e2f2cf msgid "(The low-level helpers are documented but not part of the public API.)" msgstr "(低レベルのヘルパーは文書化されていますが、パブリック API の一部ではありません。)" -#: ../../File_Transfer/File_Transfer.rst:864 60355543ac904504af8529431ce2c1fa +#: ../../File_Transfer/File_Transfer.rst:875 60355543ac904504af8529431ce2c1fa msgid "See Also" msgstr "関連項目" -#: ../../File_Transfer/File_Transfer.rst:866 7a5902ad3de64bf79d54d7f2f83ecbfb +#: ../../File_Transfer/File_Transfer.rst:877 7a5902ad3de64bf79d54d7f2f83ecbfb msgid "" "For more information about the TCP server and client base classes, please " "refer to:" msgstr "TCP サーバーおよびクライアントの基本クラスの詳細については、以下を参照してください。" -#: ../../File_Transfer/File_Transfer.rst:870 f10c29f467b849e8b4254998b44f99ba +#: ../../File_Transfer/File_Transfer.rst:881 f10c29f467b849e8b4254998b44f99ba msgid ":doc:`../Network_APIs/TCP_Server_APIs`" msgstr ":doc:`../Network_APIs/TCP_Server_APIs`" -#: ../../File_Transfer/File_Transfer.rst:871 335ba244d28342449db065c252d7e14c +#: ../../File_Transfer/File_Transfer.rst:882 335ba244d28342449db065c252d7e14c msgid ":doc:`../Network_APIs/TCP_Client_APIs`" msgstr ":doc:`../Network_APIs/TCP_Client_APIs`" -#: ../../File_Transfer/File_Transfer.rst:873 92d0227c356447a098cba072d5b43c98 +#: ../../File_Transfer/File_Transfer.rst:884 92d0227c356447a098cba072d5b43c98 msgid "" "For details on port allocation, see the Port Allocation API sections in " "those documents." diff --git a/docs/locale/ja/LC_MESSAGES/Instance_Setup/Instance_Setup.po b/docs/locale/ja/LC_MESSAGES/Instance_Setup/Instance_Setup.po index d4f19d7..bda4593 100644 --- a/docs/locale/ja/LC_MESSAGES/Instance_Setup/Instance_Setup.po +++ b/docs/locale/ja/LC_MESSAGES/Instance_Setup/Instance_Setup.po @@ -8,20 +8,20 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-30 23:45+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.18.0\n" -#: ../../Instance_Setup/Instance_Setup.rst:3 707444a8cdb246fd81a189c038518c82 +#: ../../Instance_Setup/Instance_Setup.rst:3 552b0fbec3774a638b0029ce5fd72949 msgid "Flow Setup Launcher" msgstr "フローセットアップランチャー" -#: ../../Instance_Setup/Instance_Setup.rst:5 a611d1bbc1bc4b12a9db0997909626bc +#: ../../Instance_Setup/Instance_Setup.rst:5 31952c3324264ea28368c7725796ff63 msgid "" "The ``flow_setup.py`` script is a launcher for the TCP server/client " "framework defined in ``connect_tcp.py``. It allows you to quickly spawn a " @@ -34,7 +34,7 @@ msgstr "" "インスタンスを迅速に生成できます。起動された各インスタンスは、個別のターミナル ウィンドウ (またはヘッドレス システムのバックグラウンド プロセス) " "で実行されます。" -#: ../../Instance_Setup/Instance_Setup.rst:12 bc0bbe4590e04437827678fe85f482fb +#: ../../Instance_Setup/Instance_Setup.rst:12 4f99dd7362fa4dcb8f83de37f327dbef msgid "" "**Note:** This launcher supports only **one server** and **one client** " "instance at a time. Adding a new server or client configuration will " @@ -43,20 +43,20 @@ msgstr "" "**注意:** このランチャーは、一度に **1 つのサーバー** と **1 つのクライアント** " "インスタンスのみをサポートします。新しいサーバーまたはクライアント構成を追加すると、同じタイプの以前の構成が完全に上書きされます。" -#: ../../Instance_Setup/Instance_Setup.rst:18 146d87f809054469be52a0d4174907fc +#: ../../Instance_Setup/Instance_Setup.rst:18 c8f9652279de42bb983523a15048596e msgid "Features" msgstr "特徴" -#: ../../Instance_Setup/Instance_Setup.rst:20 ea5ca94177b349b3852287e9327f792e +#: ../../Instance_Setup/Instance_Setup.rst:20 fba5b9ea870c44fabf69022a45cdaebc msgid "" "**Interactive mode** – step‑by‑step creation of a server or client instance." msgstr "**対話型モード** – サーバーまたはクライアント インスタンスを段階的に作成します。" -#: ../../Instance_Setup/Instance_Setup.rst:22 bb06f53ddbea4dd09b99e288569f858d +#: ../../Instance_Setup/Instance_Setup.rst:22 df9cbf9e1d024f9386a4b6278c82249e msgid "**Command‑line mode** – launch with all parameters in one command." msgstr "**コマンドライン モード** – 1 つのコマンドですべてのパラメータを指定して起動します。" -#: ../../Instance_Setup/Instance_Setup.rst:24 11bd4b3ce97743e0857c109ae9bc2286 +#: ../../Instance_Setup/Instance_Setup.rst:24 5f7cba6df26144e9840d140f94065b33 msgid "" "**Persistent configuration** – stores the latest instance definitions in " "``setup.json`` (same directory as the script). Each type (server/client) " @@ -65,7 +65,7 @@ msgstr "" "**永続的な構成** – 最新のインスタンス定義を「setup.json」(スクリプトと同じディレクトリ) に保存します。各タイプ " "(サーバー/クライアント) は **1 つのみ** 構成を保持し、更新のたびに上書きされます。" -#: ../../Instance_Setup/Instance_Setup.rst:29 7b3f201c93c349719f4128f05cc194bf +#: ../../Instance_Setup/Instance_Setup.rst:29 be9bc008af594306be21e731d6aefee3 msgid "" "**Cross‑platform** – supports Windows (cmd), Linux (gnome‑terminal, xterm, " "or background), and macOS (Terminal.app)." @@ -73,7 +73,7 @@ msgstr "" "**クロスプラットフォーム** – Windows (cmd)、Linux (gnome-" "terminal、xterm、またはバックグラウンド)、macOS (ターミナル.app) をサポートします。" -#: ../../Instance_Setup/Instance_Setup.rst:32 89b17f1fa60241a288381f74072aa273 +#: ../../Instance_Setup/Instance_Setup.rst:32 57005cb0d04f47b2a8f10ad0bc9c7b43 msgid "" "**Complete parameter support** – all parameters accepted by " "``TCP_Server_Base`` and ``TCP_Client_Base`` can be stored in ``setup.json`` " @@ -82,41 +82,41 @@ msgstr "" "**完全なパラメータのサポート** – ``TCP_Server_Base`` および ``TCP_Client_Base`` " "によって受け入れられるすべてのパラメータは、微調整のために ``setup.json`` に保存できます。" -#: ../../Instance_Setup/Instance_Setup.rst:37 ca859ffa1d4140cab68092170525c59f +#: ../../Instance_Setup/Instance_Setup.rst:37 2ff27919a6f64e8fb610158de5ffdf69 msgid "Usage" msgstr "使用法" -#: ../../Instance_Setup/Instance_Setup.rst:40 1935db82ef7f4dbeb08d386a49aba876 +#: ../../Instance_Setup/Instance_Setup.rst:40 dddde4f2d7084bc48efa1296cfd3f22c msgid "Interactive Mode" msgstr "インタラクティブモード" -#: ../../Instance_Setup/Instance_Setup.rst:42 86f8dafb1c9d4adaa4666b1dea52af23 +#: ../../Instance_Setup/Instance_Setup.rst:42 f060e64959674f52b223bbc4b01d568c msgid "Run the script without any arguments:" msgstr "引数なしでスクリプトを実行します。" -#: ../../Instance_Setup/Instance_Setup.rst:48 d516fe21071e448da15df91f07353fc8 +#: ../../Instance_Setup/Instance_Setup.rst:48 dd76918a5ffd44838c1b0c44595ae55b msgid "The script will ask you to:" msgstr "スクリプトは次のことを要求します。" -#: ../../Instance_Setup/Instance_Setup.rst:50 41684f03f6d7460f87fed8f4bef8f1e5 +#: ../../Instance_Setup/Instance_Setup.rst:50 80847dcc3e404a779b2e693cf8de50d5 msgid "Choose the type (0 for Server, 1 for Client)." msgstr "タイプを選択します (サーバーの場合は 0、クライアントの場合は 1)。" -#: ../../Instance_Setup/Instance_Setup.rst:51 750440862d7545e9bc4f137f36983fd6 +#: ../../Instance_Setup/Instance_Setup.rst:51 cebc8ba62d1a48a69d8019c3862dc18b msgid "Enter the bind address and port (``host:port``)." msgstr "バインドアドレスとポート (``host:port``) を入力します。" -#: ../../Instance_Setup/Instance_Setup.rst:52 a938572ed1dd43f7988e382ec9fea306 +#: ../../Instance_Setup/Instance_Setup.rst:52 4311b66e1b974b4596f2693908ba3109 msgid "If Client, also enter the server address and port to connect to." msgstr "クライアントの場合は、接続するサーバーのアドレスとポートも入力します。" -#: ../../Instance_Setup/Instance_Setup.rst:53 3f30b7f3c46e42c19eae6344b424e1f7 +#: ../../Instance_Setup/Instance_Setup.rst:53 65c194c1b655417c921c1e03daf7a17c msgid "" "Decide whether to add another instance (if you add the same type again, the " "previous configuration of that type is overwritten)." msgstr "別のインスタンスを追加するかどうかを決定します (同じタイプを再度追加すると、そのタイプの以前の構成が上書きされます)。" -#: ../../Instance_Setup/Instance_Setup.rst:55 e8b6550fe5eb426f8478c1b3fef81160 +#: ../../Instance_Setup/Instance_Setup.rst:55 6bb60d44328244d4ab20f3ae5ba2b827 msgid "" "If ``setup.json`` already exists, you will be prompted to either reuse the " "existing configuration (launch the stored instances) or overwrite it with " @@ -125,54 +125,100 @@ msgstr "" "``setup.json`` がすでに存在する場合は、既存の設定を再利用する (保存されたインスタンスを起動する) " "か、新しい定義で上書きするかを尋ねるメッセージが表示されます。" -#: ../../Instance_Setup/Instance_Setup.rst:60 b222bf87298747c6b526d72e1459ad83 +#: ../../Instance_Setup/Instance_Setup.rst:60 9aeb2efa913341a89212e86cb7419e5a msgid "" "**Important:** When you choose to overwrite, the old server/client " "configuration is **completely replaced** by the new one. There is no " "merging." msgstr "**重要:** 上書きを選択すると、古いサーバー/クライアント構成は新しい構成に**完全に置き換えられます**。合併はありません。" -#: ../../Instance_Setup/Instance_Setup.rst:65 ad560cd1d404495583c74b5929235453 +#: ../../Instance_Setup/Instance_Setup.rst:65 b5cd8be69a40420a89cf7ad69631cd45 msgid "Command‑line Mode" msgstr "コマンドラインモード" -#: ../../Instance_Setup/Instance_Setup.rst:67 50a9125f52a54c9b82c66cf616e06d71 +#: ../../Instance_Setup/Instance_Setup.rst:67 d798ec606de2454da5f10e9d0a7e677c msgid "Use the following options:" msgstr "次のオプションを使用します。" -#: ../../Instance_Setup/Instance_Setup.rst:82 f7f3b3263fdf457c908ea18b74dccea9 +#: ../../Instance_Setup/Instance_Setup.rst:70 4d5c1aebbede473c8598cbc45f8deb20 +msgid "Option" +msgstr "オプション" + +#: ../../Instance_Setup/Instance_Setup.rst:70 a50ba126b168482997fae00497252fd4 +msgid "Description" +msgstr "内容" + +#: ../../Instance_Setup/Instance_Setup.rst:72 7755904907ee46f6a34144360ed876f7 +#, python-brace-format +msgid "``--type {0,1}``" +msgstr "''-- type {0,1 }''" + +#: ../../Instance_Setup/Instance_Setup.rst:72 e0533cce8d9f4586aa838073b1ed3e2a +msgid "**Required.** 0 = Server, 1 = Client." +msgstr "**必須。** 0 =サーバー、1 =クライアント。" + +#: ../../Instance_Setup/Instance_Setup.rst:74 7cc185b424ee489282d9d85571d54fae +msgid "``--setup_addr_port``" +msgstr "''-- setup_addr_port ''" + +#: ../../Instance_Setup/Instance_Setup.rst:74 37df8730dce9408a87ba48b775eb2b0a +#, fuzzy +msgid "**Required.** Bind address and port (e.g. ``127.0.0.1:8000``)." +msgstr "バインドアドレスとポート (``host:port``) を入力します。" + +#: ../../Instance_Setup/Instance_Setup.rst:77 388a3dd952ac47a78598792ef7d587bc +msgid "``--connect_addr_port``" +msgstr "''-- connect_addr_port ''" + +#: ../../Instance_Setup/Instance_Setup.rst:77 b4f319ec304e4a26a09ac62b90db0a07 +#, fuzzy +msgid "Required for Client only. Server address and port to connect to." +msgstr "クライアントの場合は、接続するサーバーのアドレスとポートも入力します。" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7bb925846a36488d945c3442889d83c0 +msgid "``--setup_num``" +msgstr "''-- setup_num ''" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7f3e0b1925924583acf7acb25ec6e183 +msgid "" +"*Ignored.* The script always launches a single instance. This flag is " +"accepted for compatibility but has no effect." +msgstr "*無視。*スクリプトは常に単一のインスタンスを起動します。このフラグは互換性のために受け入れられますが、効果はありません。" + +#: ../../Instance_Setup/Instance_Setup.rst:86 99952fa68b694654b82a309a26419152 msgid "Examples" msgstr "例" -#: ../../Instance_Setup/Instance_Setup.rst:84 13847166f8d54c9db943e1e55cf66c55 +#: ../../Instance_Setup/Instance_Setup.rst:88 9c77e76f40a940e99e754d27e3bc1b05 msgid "**Launch a single server** on ``127.0.0.1:8000``:" msgstr "**「127.0.0.1:8000」で単一サーバーを起動**:" -#: ../../Instance_Setup/Instance_Setup.rst:90 aa92290b83324661a23d2343f7ca56fb +#: ../../Instance_Setup/Instance_Setup.rst:94 37df8730dce9408a87ba48b775eb2b0a msgid "" "**Launch a client** bound to port ``9000``, connecting to a server at " "``127.0.0.1:8000``:" msgstr "**ポート ``9000`` にバインドされたクライアントを起動**し、 ``127.0.0.1:8000`` のサーバーに接続します。" -#: ../../Instance_Setup/Instance_Setup.rst:97 9d2ca3944f984434b4ecb579b075ccc4 +#: ../../Instance_Setup/Instance_Setup.rst:101 +#: 0bdae3cd584e464ab526840aa032e3a4 msgid "" "**Launch from an existing configuration** (if ``setup.json`` is present):" msgstr "**既存の設定から起動** (setup.json が存在する場合):" -#: ../../Instance_Setup/Instance_Setup.rst:105 -#: d88aa64a570f4f36baf5c7c92d4bd861 +#: ../../Instance_Setup/Instance_Setup.rst:109 +#: ff45af42eb7d4d3faa8640a18bfd61f6 msgid "Configuration File" msgstr "設定ファイル" -#: ../../Instance_Setup/Instance_Setup.rst:107 -#: cfb0db3d5e23418d9c93ddb2d34548d4 +#: ../../Instance_Setup/Instance_Setup.rst:111 +#: 39e34a4760354d78b87a8e050029e197 msgid "" "The script writes a file named ``setup.json`` in the same directory. Its " "structure is:" msgstr "スクリプトは同じディレクトリに「setup.json」という名前のファイルを書き込みます。その構造は次のとおりです。" -#: ../../Instance_Setup/Instance_Setup.rst:131 -#: 0a2b6c0e312a49de8ed7838e045b3b66 +#: ../../Instance_Setup/Instance_Setup.rst:135 +#: 9e72e7ecd43c498299e24f5598b6f2b8 msgid "" "**Each list contains at most one object.** When a new server or client " "configuration is added, the entire list for that type is replaced." @@ -180,13 +226,13 @@ msgstr "" "**各リストには最大 1 つのオブジェクトが含まれます。** " "新しいサーバーまたはクライアント構成が追加されると、そのタイプのリスト全体が置き換えられます。" -#: ../../Instance_Setup/Instance_Setup.rst:136 -#: 1a256391599446d99c3cc375639f251c +#: ../../Instance_Setup/Instance_Setup.rst:140 +#: 8731a083a201487bb579beb4a238a0db msgid "Custom Parameters" msgstr "カスタムパラメータ" -#: ../../Instance_Setup/Instance_Setup.rst:138 -#: 36cefa4820cd42ee93fdb560be1b1032 +#: ../../Instance_Setup/Instance_Setup.rst:142 +#: 1568543fd757484da86f12a72ccb58b5 msgid "" "You can manually edit ``setup.json`` to include any parameter accepted by " "``TCP_Server_Base`` or ``TCP_Client_Base`` (see the source code for the full" @@ -202,13 +248,13 @@ msgstr "" "(スクリプトは既存の構成を読み取り、ユーザーが指定した値で更新するためですが、上書きを選択すると、古い構成は破棄され、新しいフィールドのみが保存されます。そのため、カスタム" " パラメーターが必要な場合は、最初の起動後に追加するか、ファイルを手動で編集する必要があります)。" -#: ../../Instance_Setup/Instance_Setup.rst:150 -#: 6b90a320b39147c7ac5287c51b028da3 +#: ../../Instance_Setup/Instance_Setup.rst:154 +#: c62b1bfe8ceb40f8865a34ac87b4ba18 msgid "Extension Protocols and Startup Mode" msgstr "拡張プロトコルと起動モード" -#: ../../Instance_Setup/Instance_Setup.rst:152 -#: 81cf1090db0240e19aecfb16cdafb622 +#: ../../Instance_Setup/Instance_Setup.rst:156 +#: e6a7dcb4ea184e828e6667ae6505d872 msgid "" "Two extension protocols ship with the launcher and are loaded automatically " "for every instance whose ``setup.json`` entry sets " @@ -217,60 +263,75 @@ msgstr "" "ランチャーには 2 つの拡張プロトコルが同梱されており、``setup.json`` のエントリで ``is_extend_command=True``" " が設定されているすべてのインスタンスに自動的にロードされます:" -#: ../../Instance_Setup/Instance_Setup.rst:156 -#: e89028b886064e8ab1e94e5e6a6cdaa0 +#: ../../Instance_Setup/Instance_Setup.rst:160 +#: 7159f768fc9a4ac199bfe0a4d8478fba #, fuzzy -msgid "``command_control_extension_tcp.py`` – remote command" +msgid "" +"``command_control_extension_tcp.py`` – remote command execution with per-" +"client log collection (``/command``)." msgstr "" "- ``command_control_extension_tcp.py`` – クライアントごとのログ収集を伴うリモートコマンド実行 " "(``/command``)。" -#: ../../Instance_Setup/Instance_Setup.rst:157 -#: 86a457e4e5bf4791bcbef5bb12612391 +#: ../../Instance_Setup/Instance_Setup.rst:162 +#: d2d277053c74470abc949b891493a5f0 +#, fuzzy msgid "" -"execution with per-client log collection (``/command``). - " -"``forward_extension_tcp.py`` – forwarding messages, files, multiple files, " -"folders and multiple folders to any number of destination clients " -"(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " -"``/folder_forward``, ``/multiple_folder_forward``)." +"``forward_extension_tcp.py`` – forwarding files, multiple files, folders and" +" multiple folders to any number of destination clients (``/file_forward``, " +"``/multiple_file_forward``, ``/folder_forward``, " +"``/multiple_folder_forward``)." msgstr "" "クライアントごとのログ収集 (``/command``) を使用して実行します。 - `forward_extension_tcp.py` – " "メッセージ、ファイル、複数のファイル、フォルダー、および複数のフォルダーを任意の数の宛先クライアントに転送します " "(`/send_msg_forward`、`/file_forward`、`/multiple_file_forward`、`/folder_forward`、`/multiple_folder_forward`)。" -#: ../../Instance_Setup/Instance_Setup.rst:164 -#: baee7200e5634507a28b2f69309c3c49 +#: ../../Instance_Setup/Instance_Setup.rst:168 +#: a8194174e5fd4dcf87ede716eaada9a4 +msgid "" +"Plain-message forwarding is native to the TCP protocol (no extension " +"needed): the client-only command ``/forward_send_msg`` relays messages to " +"the listed destination clients through the server." +msgstr "" +"プレーンメッセージ転送はTCPプロトコルにネイティブです(拡張子は必要ありません)。クライアント専用コマンド''/ forward_send_msg " +"''は、サーバーを介してリストされた宛先クライアントにメッセージをリレーします。" + +#: ../../Instance_Setup/Instance_Setup.rst:173 +#: 98297b76f3404736b92b6c507726a50a msgid "" "With ``is_extend_command=False`` (the default) only the raw TCP protocol is " "started." msgstr "``is_extend_command=False`` (デフォルト) の場合、生の TCP プロトコルのみが起動されます。" -#: ../../Instance_Setup/Instance_Setup.rst:167 -#: f140a881acba4933bd83af5bb35737d1 +#: ../../Instance_Setup/Instance_Setup.rst:176 +#: 9b5ab0319a9843f1b39721d3a96d1777 msgid "" "The ``is_input_command_in_console`` flag selects how the instance is " "started:" msgstr "``is_input_command_in_console`` フラグは、インスタンスの起動方法を選択します:" -#: ../../Instance_Setup/Instance_Setup.rst:170 -#: a40aa238baac4503a0dc0c9cea0745ee -msgid "``True`` (default) – ``start_TCP_Server()`` /" -msgstr "``True`` (デフォルト) – ``start_TCP_Server()`` /" +#: ../../Instance_Setup/Instance_Setup.rst:179 +#: 1ec730c995b1454da1b208331d9ca8fe +msgid "" +"``True`` (default) – ``start_TCP_Server()`` / ``start_TCP_client()`` is " +"called directly and the console input loop runs in its own thread." +msgstr "" +"'' True '' (デフォルト) – '' START_TCP_SERVER ()''/'' START_TCP_CLIENT " +"()''が直接呼び出され、コンソール入力ループは独自のスレッドで実行されます。" -#: ../../Instance_Setup/Instance_Setup.rst:171 -#: fca5c2c0fe914740a931957b30a9927b +#: ../../Instance_Setup/Instance_Setup.rst:182 +#: 3cfb2eba312943cea85388f4e9faf40d #, fuzzy msgid "" -"``start_TCP_client()`` is called directly and the console input loop runs in" -" its own thread. - ``False`` – the instance runs in a background thread and " -"the launcher keeps the process alive until the instance stops (useful for " -"headless deployments)." +"``False`` – the instance runs in a background thread and the launcher keeps " +"the process alive until the instance stops (useful for headless " +"deployments)." msgstr "" "``start_TCP_client()`` が直接呼び出され、コンソール入力ループは独自のスレッドで実行されます。 - ``False`` – " "インスタンスはバックグラウンド スレッドで実行され、ランチャーはインスタンスが停止するまでプロセスを維持します (ヘッドレス展開に便利)。" -#: ../../Instance_Setup/Instance_Setup.rst:177 -#: c2925f0f85154040bcdd1252a228fd7f +#: ../../Instance_Setup/Instance_Setup.rst:186 +#: 3b3482fa903f4afd81234e588a229c1f msgid "" "Both extensions also expose injectable registration " "(``setup_server_commands(instance)`` / ``setup_client_commands(instance)``) " @@ -285,63 +346,56 @@ msgstr "" "``server_setup(instance=None, is_input_command_in_console=True)`` " "も公開しているため、コードから複数の拡張機能を同じインスタンスにロードできます。" -#: ../../Instance_Setup/Instance_Setup.rst:186 -#: e58290a2c5f3492bafa47f15b7c2958f +#: ../../Instance_Setup/Instance_Setup.rst:195 +#: 1dc7b46c0e3047d2885573cc4f55e7c6 msgid "Internal Operation" msgstr "内部操作" -#: ../../Instance_Setup/Instance_Setup.rst:188 -#: 6655764b01644b92910338c01d38b0cb -msgid "Each instance is launched in a new terminal window" +#: ../../Instance_Setup/Instance_Setup.rst:197 +#: 8870bc6b731d40a095a40faed0c8f16d +#, fuzzy +msgid "" +"Each instance is launched in a new terminal window (or background process)." msgstr "各インスタンスは新しいターミナル ウィンドウで起動されます" -#: ../../Instance_Setup/Instance_Setup.rst:189 -#: 6824028a99254c77a55fb56d51b5e9a5 -msgid "(or background process)." -msgstr "(またはバックグラウンドプロセス)。" - -#: ../../Instance_Setup/Instance_Setup.rst:190 -#: 0a09353648d44ba2a840fc1cf8a850bc -msgid "The configuration is passed via a temporary JSON" +#: ../../Instance_Setup/Instance_Setup.rst:199 +#: 13c720a93ba54e8ca330fce3a078bb5b +#, fuzzy +msgid "" +"The configuration is passed via a temporary JSON file to avoid shell " +"escaping issues." msgstr "設定は一時的な JSON 経由で渡されます" -#: ../../Instance_Setup/Instance_Setup.rst:191 -#: f6327281e35a4304b404563d00301a1a -msgid "file to avoid shell escaping issues." -msgstr "シェルエスケープの問題を回避するためのファイル。" - -#: ../../Instance_Setup/Instance_Setup.rst:192 -#: 997e364f35714b18b1e47af641ce58db -msgid "If an instance fails to start, the error is" -msgstr "インスタンスの起動に失敗した場合、次のエラーが発生します。" - -#: ../../Instance_Setup/Instance_Setup.rst:193 -#: 4404c088296e4c35b49ed59eca3b6678 -msgid "displayed and the window pauses for inspection." +#: ../../Instance_Setup/Instance_Setup.rst:201 +#: 9566eb3cb70e4b77bcbdb584f7e0b6fc +#, fuzzy +msgid "" +"If an instance fails to start, the error is displayed and the window pauses " +"for inspection." msgstr "と表示され、検査のためにウィンドウが一時停止します。" -#: ../../Instance_Setup/Instance_Setup.rst:196 -#: 2b8f982006184d36b06d4ac5579a36f4 +#: ../../Instance_Setup/Instance_Setup.rst:205 +#: 5f0f0f6a41384356986a1182f14a0a4c msgid "Requirements" msgstr "要件" -#: ../../Instance_Setup/Instance_Setup.rst:198 -#: f10fabd9883942b780cd0822da245837 +#: ../../Instance_Setup/Instance_Setup.rst:207 +#: 49ceb74e41104b00971cd9edecef08ae msgid "Python 3.6+" msgstr "Python 3.6+" -#: ../../Instance_Setup/Instance_Setup.rst:199 -#: f464e1a571fa46388937f3236b36d3bf +#: ../../Instance_Setup/Instance_Setup.rst:208 +#: 541cd1be44c044f3880eb220a87c06b7 msgid "The ``network_api.connect_tcp`` module must be" msgstr "``network_api.connect_tcp`` モジュールは次のようにする必要があります。" -#: ../../Instance_Setup/Instance_Setup.rst:200 -#: fc6ca227cd904333916e81b4a7093acc +#: ../../Instance_Setup/Instance_Setup.rst:209 +#: 427fef8d7c5047fc934681026969639a msgid "importable (the script imports ``TCP_Server_Base``" msgstr "importable (スクリプトは ``TCP_Server_Base`` をインポートします)" -#: ../../Instance_Setup/Instance_Setup.rst:201 -#: dcc9c410853247b0a3302c2c41f6fe13 +#: ../../Instance_Setup/Instance_Setup.rst:210 +#: fb35af261d664664bdb3c1b3aaac3e83 msgid "and ``TCP_Client_Base`` from there)." msgstr "そこから ``TCP_Client_Base`` となります)。" @@ -361,3 +415,15 @@ msgstr "そこから ``TCP_Client_Base`` となります)。" #~ msgstr "" #~ "- ``True`` (デフォルト) – ``start_TCP_Server()`` / ``start_TCP_client()`` " #~ "が直接呼び出され、コンソール入力ループは独自のスレッドで実行されます。" + +#~ msgid "``True`` (default) – ``start_TCP_Server()`` /" +#~ msgstr "``True`` (デフォルト) – ``start_TCP_Server()`` /" + +#~ msgid "(or background process)." +#~ msgstr "(またはバックグラウンドプロセス)。" + +#~ msgid "file to avoid shell escaping issues." +#~ msgstr "シェルエスケープの問題を回避するためのファイル。" + +#~ msgid "If an instance fails to start, the error is" +#~ msgstr "インスタンスの起動に失敗した場合、次のエラーが発生します。" diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.add_extension.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.add_extension.po new file mode 100644 index 0000000..775b857 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.add_extension.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.add_extension.rst:2 239fa5edd0d944bd884b21d803e0f4a1 +msgid "PyFlow.add\\_extension module" +msgstr "" + +#: PyFlow.add_extension.copy_extension_files:1 c4a96a18a6754f1fb4c5c3a9ff9dd518 +#: of +msgid "Validate extension path(s) and return them as a list." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst 4b8e1285572947c4a34dbcd9e22fb52c +#: 78eb00a995d84b028e95127f753b4fb5 PyFlow.add_extension.remove_extension +#: dff4aabd6aad43e188c9fc9b73d9142b of +msgid "Parameters" +msgstr "" + +#: 49c2e6f1496d433ab7a9d162802419f1 5de3fc7a955443cb9bef23851780925f +#: PyFlow.add_extension.add_extension:3 +#: PyFlow.add_extension.copy_extension_files:3 +#: PyFlow.add_extension.remove_extension:3 b82537ecca1e4f418a5a6aacacdd900c of +msgid "a single path string or a list of path strings." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst fa91fb96a98f4374b2da5085d1373ef4 +msgid "Returns" +msgstr "" + +#: 450992bbbe91432fb44b0265e58d8f59 PyFlow.add_extension.copy_extension_files:5 +#: of +msgid "The original paths as a list (extensions are not copied)." +msgstr "" + +#: 487ed0fee55540c796c92b88d0a8b2ea +#: PyFlow.add_extension.add_added_extension_logs:1 of +msgid "Append paths to the extension registration log file." +msgstr "" + +#: PyFlow.add_extension.add_extension:1 cd718206d465487ebd43c17e796c4da5 of +msgid "Register extension file(s) in added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.remove_extension:1 bb4313796aff49a1b96a37d66912f18a of +msgid "Remove registered extension path(s) from added_extensions.json." +msgstr "" + +#: 6520ce551e5f40e6afe59c549d15e493 PyFlow.add_extension.remove_extension:5 of +msgid "If the registration file does not exist, this is a no-op." +msgstr "" + +#: 26339cf4e8a041f798d969a890592971 +#: PyFlow.add_extension.load_registered_extensions:1 of +msgid "Load every registered extension from added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.load_registered_extensions:3 +#: e53cceb4fe384c1fa10ddf1905998823 of +msgid "" +"For each registered path, the module is imported dynamically and its " +"``setup_server_commands(instance)`` or " +"``setup_client_commands(instance)`` is called, depending on " +"*instance_type*." +msgstr "" + +#: 11efbd89b0254bb190c21005396f53dd +#: PyFlow.add_extension.load_registered_extensions:7 of +msgid "" +"Raises ImportError if the JSON file is reachable but a module cannot be " +"imported or loaded, or if the required setup function is missing." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po new file mode 100644 index 0000000..08b8c15 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po @@ -0,0 +1,36 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.command_control_extension_tcp.rst:2 +#: a6084f2941f34f579f28c55e9dfb768d +msgid "PyFlow.command\\_control\\_extension\\_tcp module" +msgstr "" + +#: 9209095513ec402980427a0ddd988219 +#: PyFlow.command_control_extension_tcp.setup_server_commands:1 of +msgid "Register the control-extension commands on a server instance." +msgstr "" + +#: 114b0a54c29747fd88bc2852eab174bf +#: PyFlow.command_control_extension_tcp.setup_client_commands:1 of +msgid "Register the control-extension commands on a client instance." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.flow_setup.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.flow_setup.po new file mode 100644 index 0000000..a17c586 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.flow_setup.po @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.flow_setup.rst:2 a6a54598744740409fe0b28f73834ffe +msgid "PyFlow.flow\\_setup module" +msgstr "" + +#: 586460f36f3b4654abb0db6b5d77b39d PyFlow.flow_setup.launch_web_tool:1 of +msgid "Launch the transfer_web launcher (``kind`` = \"server\" or \"client\")." +msgstr "" + +#: PyFlow.flow_setup.launch_web_tool:3 ef1274ba70124aeeadee3d4cfcca99c2 of +msgid "" +"The web tool is a Flask app that opens a browser UI, so it runs in its " +"own process (a terminal window when one is available, otherwise detached)" +" and the launcher returns immediately." +msgstr "" + +#: 7086887643164f229f919546dceb0e36 PyFlow.flow_setup.edit_existing_instances:1 +#: of +msgid "Vim-style editor to delete/change existing instances." +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:3 d04ffe1cf853403292908fc69962f7dc +#: of +msgid "Returns (status, servers, clients):" +msgstr "" + +#: 5a751731515c4adfba9787fdd5e93215 PyFlow.flow_setup.edit_existing_instances:4 +#: of +msgid "status == \"saved\" -> setup.json was written (:w / :wq); keep the" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:5 e61a6437dae14d1e9f9ac4e6848cd444 +#: of +msgid "returned edited lists" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:6 bb9424a8ea894dbb888df4fa2e0875ed +#: of +msgid "status == \"discarded\" -> the editor was exited without saving" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:7 d0d81a426f754475922c3e9f4aee6627 +#: of +msgid "(:q! / :q) and the original lists are returned unchanged" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po new file mode 100644 index 0000000..adae51a --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.forward_extension_tcp.rst:2 +#: 39b0ca283c7c48b08f66a67eb80766b0 +msgid "PyFlow.forward\\_extension\\_tcp module" +msgstr "" + +#: PyFlow.forward_extension_tcp:1 ad6b75f791c543dea48894cd8bac6941 of +msgid "Forward extension for the TCP protocol." +msgstr "" + +#: PyFlow.forward_extension_tcp:3 ff4264b5fba24d7d93e9bb3e398edfd5 of +msgid "" +"Disk-based, upload-then-push forwarding of files and folders to a list of" +" destination clients. This is deliberately a second implementation of " +"file forwarding: the native TCP protocol already streams files and " +"folders in memory (``/forward_file`` / ``/forward_folder`` on a client " +"console, relayed by the server as ``/forward_item`` with no disk I/O on " +"the server), while this extension uploads the data to the server's " +"transfer directory first and then asks the server to push the stored " +"copies. Plain-message forwarding is native as well (the client-only " +"command ``/forward_send_msg``, relayed by the server), so no string " +"forwarding lives here." +msgstr "" + +#: 2f49002886224f5bb68eb09f2c4a8a30 PyFlow.forward_extension_tcp:14 of +msgid "Transfer families added by this extension:" +msgstr "" + +#: PyFlow.forward_extension_tcp:16 ab1c45d0c28345c2898a54c66c98416b of +msgid "/file_forward <(ip, port)> ..." +msgstr "" + +#: 93ef2915b1fa4bd9b4b42e8e6ec747f8 PyFlow.forward_extension_tcp:17 of +msgid "forward one file to every listed destination" +msgstr "" + +#: 9773fb5cd9414f0aabb496ae3a32005a PyFlow.forward_extension_tcp:18 of +msgid "/multiple_file_forward ... <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:19 ee54881259274d6b9085bf71b44d659a of +msgid "forward several files to every listed destination" +msgstr "" + +#: 19144093ca8d45e988dfcfe09221bf6a PyFlow.forward_extension_tcp:20 of +msgid "/folder_forward <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:21 b73d9263c9874b1e9ea5a714138d72d8 of +msgid "forward one folder (structure preserved) to every destination" +msgstr "" + +#: 4f7e8bb4b31c4eddbb78d815e85f8795 PyFlow.forward_extension_tcp:22 of +msgid "/multiple_folder_forward ... <(ip, port)> ..." +msgstr "" + +#: 9e95e860d39744f8ba3d52ddaf357943 PyFlow.forward_extension_tcp:23 of +msgid "forward several folders to every listed destination" +msgstr "" + +#: 612162f14a574132bf396a1b6ea853f1 PyFlow.forward_extension_tcp:25 of +msgid "" +"Items come first, destinations last; every destination is written as a " +"Python address tuple, e.g. ``\"('127.0.0.1', 3000)\"``. There is no limit" +" on the number or size of items or destinations." +msgstr "" + +#: 1b0ec7328493432fb876117b1f76bb3b PyFlow.forward_extension_tcp:29 of +msgid "" +"The commands are only available on the client console: they are " +"registered in the \"client\" handler group, so typing them on the server " +"console is rejected as an unrecognized command. Forwarding goes through " +"the server - the client uploads the data over the normal transfer channel" +" (the server stores it in its transfer directory) and then asks the " +"server to push it to the destinations, which receive it through the main " +"protocol's own receive paths. Destinations that are unreachable (not " +"connected to the server, or the server itself, which is never in the " +"client table) are skipped and the remaining destinations are still " +"served." +msgstr "" + +#: 443d9fe7a21148d984a0e3f2f3cb2c28 +#: PyFlow.forward_extension_tcp.setup_client_commands:1 of +msgid "Register the file/folder forward commands on a client instance." +msgstr "" + +#: 5dbc099afeb54fbb859bf5760331adfd +#: PyFlow.forward_extension_tcp.setup_client_commands:3 of +msgid "" +"Message forwarding (``/forward_send_msg``) is native and needs no setup. " +"Each command binds its transfer kind and single/multiple policy into the " +"shared handler via functools.partial; where_to_run=\"client\" makes them " +"fire from console input only." +msgstr "" + +#: 392556d0850f44b1b64dae0fe65a748c +#: PyFlow.forward_extension_tcp.setup_server_commands:1 of +msgid "Register the file/folder forward relays on a server instance." +msgstr "" + +#: 399725c6728441568702225abc50b2c4 +#: PyFlow.forward_extension_tcp.setup_server_commands:3 of +msgid "" +"The message relay (``/forward_send_msg``) is native and needs no setup. " +"These handlers are triggered by relay requests sent by clients, i.e. they" +" live in the \"server\" group: messages coming in from other instances " +"are dispatched there. The /xxx_forward commands themselves stay in the " +"client group, so typing them on the server console is rejected as " +"unrecognized." +msgstr "" + +#: 337c29db84fe4d3099a2e3d5e135d0d1 PyFlow.forward_extension_tcp.client_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable client (mirrors the control " +"extension)." +msgstr "" + +#: 99ee11955438459d84f5c1ae6f3fedbb PyFlow.forward_extension_tcp.server_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable server (mirrors the control " +"extension)." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po new file mode 100644 index 0000000..dd24c15 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po @@ -0,0 +1,1759 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_tcp.rst:2 +#: 1e4cd09ce3ef45ceb95b8eb81eb7493b +msgid "PyFlow.network\\_api.connect\\_tcp module" +msgstr "" + +#: 25b88cdb337c44539a843c674ba6825b PyFlow.network_api.connect_tcp:1 of +msgid "" +"TCP transport for PyFlow: the server and client base classes and the wire" +" parsers." +msgstr "" + +#: 1f1482fc5c2849a68f34c74a8b55ab79 PyFlow.network_api.connect_tcp:3 of +msgid "" +"``TCP_Server_Base`` accepts connections and dispatches inbound lines; " +"``TCP_Client_Base`` connects, sends and reads on the same conventions:" +msgstr "" + +#: 76e93df1b95547ff8bded710dc31340d PyFlow.network_api.connect_tcp:6 of +msgid "" +"one message per line, terminated by a newline; a line that starts with " +"``/`` is a command and goes to the command handlers, anything else is a " +"plain message reported to the registered message listeners;" +msgstr "" + +#: 2497ecf21ac947c78502a3839a452cb8 PyFlow.network_api.connect_tcp:9 of +msgid "" +"an RSA-encrypted channel is negotiated right after connect unless " +"``is_enable_encrypto`` is False;" +msgstr "" + +#: 9702905f1f8a4750905b2ffb96f99ec8 PyFlow.network_api.connect_tcp:11 of +msgid "" +"file/folder transfer, message forwarding and port allocation are layered " +"on the same socket and share its command namespace." +msgstr "" + +#: 4cc5743bdfe54148ad38f54f10e688c9 PyFlow.network_api.connect_tcp:14 of +msgid "" +"The forwarding extensions use the module-level parsers " +"`parse_forwarded_message`, `parse_forward_items_and_addrs`, " +"`parse_forward_originator` and `forward_skip_message`." +msgstr "" + +#: 49796529cb7f49ee8131b257212b5420 PyFlow.network_api.connect_tcp:18 of +msgid "" +"Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and " +"``TCP_Client_APIs.rst``; argument, return and exception contracts live in" +" the docstrings below." +msgstr "" + +#: 39274682c4bd46489ed6fe535b50ede5 +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:1 of +msgid "Split a ``/send_msg_from `` relay envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07da14a120314eb7a3acd1c46ddfeaca 0b005c95146946208a01640628f50a57 +#: 10904afb15ee45b29e91a2ab5da0db26 14d9353e52a54c019428fb94211f17f6 +#: 1861e8bcd5bb41489f13265860311349 1c6ba1e0968a409499b7a262c34dcc2a +#: 20b95160870648538fd617ca4ce3d2b5 2351fc654dfc452a8d07e5d566265d9f +#: 27611fb8240a422db411c043f60bdbe9 464f4d62231e4a61b1a939ad2054b13c +#: 48bd66821c14451d9a0c682d9468a5a5 4e1c4be3e22f403396d5ee1788d0e3b0 +#: 4fa9ba6e763440b8ab00832520b4d305 501a0caef71d436e84469bc1ef9c1e5f +#: 52bdf8d502cc4a31a6df5ae543ceb182 531f1e6ff68e44bcac61633e2d7c511f +#: 5404b8d0f66848c3b1db9f08a25f92c0 55fc240d149746e190ecf924a2ca6dd4 +#: 59ab6a93eceb40f7b590133c3d2b8548 6d8d8704b9c94105928dbbdf15ce9f12 +#: 70d6dae11a144ed7b7e9d8e16dbf478c 71de1bb0c4d24062a819a6fe59013d34 +#: 74c64b0da3294322b512f3155955fb7a 76f08e34cd4a42c880880cfb511fcc66 +#: 7a6a9f1497ac48f5867783d78fdae37c 7c3501da0be44d78a671e96ba4384489 +#: 7e9c7fe1ae974c1082550fb3e6e3de1f 7fc19de66098437faef7bffed3b5f752 +#: 7fdf476d99ff4f54ad35cf4bb506e47d 812d9d039fe34f71aa4b662c4511c8e9 +#: 81f8f7d346ff45639f39eb0f033103df 95d8b328540c4094a4fcc8fdc9139645 +#: a163536aaa1f49399502da52fb481666 acdf8b3b266f4eccb6ceca17a110603b +#: b097aecf6ae74e17b36fc9e806d5b26a b3db16a6c2374980ae9b071d9f3f15e6 +#: c2a18448d83f4879bcb29f51ca31bc5b c5ed2382d01042c68a5e372a4c7de2ac +#: d36c8c10afba4732969367886b8663ab d6d746e16dbd47308eea5aacd9614f15 +#: d8e94668c5d7434fa61c2ffcbe73b6af dad1324e962d49419245fe3b88c20121 +#: dd3d356c14cb462e98ea42e3502dca80 eb24a57758194ab2bdef9e95362584df +#: ef5da8d6c91c469fad0871d213999d2a ff3615b7096e46bba8797b739bb954b2 +msgid "Parameters" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:3 +#: e42a6166c82d4861b25cb55f587db28c of +msgid "Received line, e.g. ``/send_msg_from ('127.0.0.1', 3000) hello``." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07f0651994f343d7ac8ee26ae4a45c8e 0b14e887dc524d55affce5a3bd9b9f8d +#: 0cb557ccd8a6423091d0d562f1af31fd 0d1ef94754104f01a11302d72600e73a +#: 1475f9cb68fe417b9de7226406e810b5 14dcda8b059b480982e3fb03399e665b +#: 1d7731490c2f49959618924ddf4327fd 1e40a9184f8544c6b7a1472d55c66a11 +#: 24af9c4562134d16b50c664902b39dff 295718cd92e04380af38e7afaff04010 +#: 34fc7f7760d94c2f8bac262ff821b4d7 451b693e9c414cff9798a5a1592fe9cd +#: 47bbee4bd36c4f998fd04c8cc8d9c199 537ed27e714148da8ea804e3562ccd96 +#: 565b9def256a453784c749fe2ee93bc8 57dee3931014419c91d049fca185ef1b +#: 5dcfbd3c4b13401a82e19373602a4b92 635677a7aff64b79a4f69ffb4c22841c +#: 74b8a4ad713a43f2a14faf494e1886a4 7eca27378af64ce99374c606219ff337 +#: 7f6560a3959c4f069adff9d00ca33f54 855ad029af764579bf4ab14cbd430caa +#: 967377f151e84130a9778ece6228bec2 99035a24f6e94207b84545f8c71a451e +#: a90e5d6a51cd4591afeb3cd934071de0 c72891376fe2481d95c7d9ae3014ae47 +#: ca270cee6d3f4eb7ba331ab841c77f38 cb62202c244d4c08974c674c13a02dca +#: db47465f222c4f3db50a9133de7035f5 eb66bb58c7534346b105203e37b25760 +msgid "Returns" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:7 +#: f25d1962656c4f339805427e2573f84f of +msgid "" +"``(sender_id, payload)`` where ``sender_id`` is the sender's " +"``\"ip:port\"``, or None when the line is not a well-formed envelope." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:9 +#: d0d7af37e4134902ae64e10b2e43b4c1 of +msgid "``(sender_id, payload)`` where ``sender_id`` is the" +msgstr "" + +#: 09b385f0a98640c981d8563427e44d7e +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:10 of +msgid "" +"sender's ``\"ip:port\"``, or None when the line is not a well-formed " +"envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 0acb7e2291644625b8873e997a801b95 140c4a59eb0f43d5b4dc46fc2d3348a7 +#: 1412375394164f17aa5b40b7d9ecf0a4 19eb6fca79324b0791f8ec7488a5246c +#: 21c016ab955e4e6499a97f0ca86684cb 27bd312392a2404099fa44fd8787ed28 +#: 27dcdca3b6b247069ddce775ec471c70 28f7c9db48d949ef930abcedef021138 +#: 28fa8dcc5913460e8906e926c95408b9 2e363ed3ce6746c08ebfbf8bb889b623 +#: 3d654cd4b6b044baa3774f758d61da71 48e6af3ee30f47409e2837867a01ff48 +#: 50da78ed5f904c74a876d7b494241efa 50df226290614747b5568f633a883ae0 +#: 564af23329ae4b29aaea3a3638292f42 571f3b412784479a9bfdcb5034c6a39f +#: 6759afd1efe445adb4970c6265c95995 73e666bc0d164cb6a1506517d9acfdb4 +#: 752e44acb3104f9bbf4d3ff7c8bc244b 76862b0024bd42e2b012c999d92e6969 +#: 7ba45226839e40c8a00e684e7c4e07b9 7cd20d28b6794f34a0f53570fea9546a +#: afead614116b49619f1d29738a18e166 b443d67ba649468ca55f1889f18dd006 +#: bbc7b22bbf5e42f7bae149f481de9f3a c58a75656c6143f8a0cb397f24b619b2 +#: d4f12b17df174b038d554053e034cd2a e669e2ac035448fab6479e209ab51c4d +#: f278b3632fbc43f2b4cbeb7759606a68 f6ee270c707f4805b9035fa772078d19 +msgid "Return type" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:1 +#: f22254d8ca2d47ad8bc9c98664369d61 of +msgid "Split forward-command tokens into items and destination addresses." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:3 +#: b207f60051004834bab91ba392a88437 of +msgid "" +"A token of the form ``('ip', port)`` is a destination, everything else is" +" a forwarded item (message text or a path). Used by the native message " +"forwarding (``/forward_send_msg``) and by the file/folder forward " +"extension." +msgstr "" + +#: 5329fb72f5954768a6cec05ff4cccfea +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:7 of +msgid "Tokens after the command name." +msgstr "" + +#: 2aebf669048046ddb279a935bcfcfde4 +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:10 of +msgid "" +"``(items, addrs)`` in the order given; ``items`` holds texts and " +"paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: 96b96bd19fe94312bddf340fad073cbc +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:12 of +msgid "``(items, addrs)`` in the order given; ``items`` holds texts and" +msgstr "" + +#: 8e82954621d748e2b412952a7fb2753b +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:13 of +msgid "paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:1 +#: a51324658c7943158d90ed706cecc41d of +msgid "Build the console notice for a forward destination that cannot be served." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:3 +#: b541e8446e374bc8b38cd7912a1fa33c of +msgid "Destination ``(ip, port)`` that is unreachable or is the server itself." +msgstr "" + +#: 10e03f21d05e478d9cde0c7176f6e1b8 +#: PyFlow.network_api.connect_tcp.forward_skip_message:7 of +msgid "One-line notice for the console." +msgstr "" + +#: 8005e532fe9d491ca8ddd996889d605d +#: PyFlow.network_api.connect_tcp.parse_forward_originator:1 of +msgid "Extract the originator's ``\"ip:port\"`` from a received transfer command." +msgstr "" + +#: 2cf3052a89c645e782430f1f05fb37a1 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:3 of +msgid "" +"The server's forward relay tags every pushed ``/file`` and " +"``/file_folder`` command with the forwarding client's address tuple; a " +"direct send carries the receiver's own address instead." +msgstr "" + +#: 335f6c1b16104395ba38a33188943d8a +#: PyFlow.network_api.connect_tcp.parse_forward_originator:7 of +msgid "Received transfer command." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:9 +#: e5bd1024ad3f47f19cafc4c83a844b60 of +msgid "" +"This instance's own ``\"ip:port\"``; a command carrying it is a direct " +"send and yields None." +msgstr "" + +#: 65469492f844448fab269b701cbdb704 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:13 of +msgid "" +"Originator ``\"ip:port\"``, or None when the command carries no " +"originator (direct send or non-transfer command)." +msgstr "" + +#: 04a0882a3a5e4099a114728b4d1c79c0 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:15 of +msgid "Originator ``\"ip:port\"``, or None when the command carries no" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:16 +#: a2d34276c4f54c69981f9b51f36cc31d of +msgid "originator (direct send or non-transfer command)." +msgstr "" + +#: 7297b41c47664243b9677b32e21de07e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:1 of +msgid "TCP server: accept clients, dispatch commands, relay messages and files." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:3 +#: f5119fd32b60425bb33e4aa148ff7139 of +msgid "" +"Each accepted connection is served by `handle_client` in its own thread: " +"a line starting with ``/`` goes to `handle_command` (built-in commands " +"plus the handlers registered with `register_command`), any other line is " +"a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``." +msgstr "" + +#: 2870b0163d68487daf7526e91eb49ff6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:11 of +msgid "Address the server socket binds to." +msgstr "" + +#: 007c3d5fac544fd480ac10cc56c2f236 0929a7309fef40cab69e9dc22c376ef2 +#: 2e995e9363c54a7ba267ff3a53e10908 507062a8930e432dbcab31b039724b27 +#: 66f4496879ee419d8d748faf8cff0f05 75fcffd8750942559c93c5e590a10503 +#: 83c05847c82b492090a9279f5072434d 8e64315489764ceba6e1120da4675ffb +#: 9c28c79f3bb84af98029a17c0ffc84cd +#: PyFlow.network_api.connect_tcp.TCP_Client_Base +#: PyFlow.network_api.connect_tcp.TCP_Server_Base +#: cf754e752b6c445989b3301b807fc9eb d61fb222783b4f898eb1e757631a1c9a of +msgid "type" +msgstr "" + +#: 2dcb98253fbf428b96fdb5b720769c02 568be916ae0340289a6569e718e7cbf2 +#: 904732a26bcc4835bd3429f0015ae8a4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:14 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:26 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:13 of +msgid "str" +msgstr "" + +#: 6ac74cca7b0f443ca89f3d2c3bbe1aaa +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:17 of +msgid "First port considered for binding and for allocation." +msgstr "" + +#: 0f9991451920470fa3c8a74228b46bb0 9ecf88c5193c470b98dc95d184c94f92 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:19 of +msgid "int" +msgstr "" + +#: 6746407aac194c4880c7cbf82b7fa2fb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:23 of +msgid "" +"Accepted connections keyed by ``(ip, port)``; each value holds " +"``socket``, ``address``, ``id`` and ``connected_time``." +msgstr "" + +#: 7ba2aaf3fe4c42739121eb3ca2bcc79a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:26 of +msgid "dict" +msgstr "" + +#: 93556efee2aa445684af3fdc1532548a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:30 of +msgid "True while the accept loop runs." +msgstr "" + +#: 0ab0667a38254203929e173ab5024f1d 76745b91cce345b092b4e5d931d71126 +#: 839584ae46a0488a95917b35e253768a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:38 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:44 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:32 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:38 +#: f692af2051ae43cdae389a97cd91e9d7 of +msgid "bool" +msgstr "" + +#: 4ad873e4e9da4574bc068029455f62ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:36 +#: fae1c5fe3a844b69a2c7ec611073ecf9 of +msgid "Whether the RSA channel is negotiated." +msgstr "" + +#: 37e6dd691f544a80b9e0949883a2e5a8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:1 of +msgid "Create the server and, unless extended, start accepting clients." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:3 +#: bf58d54a167d45de9a18641877ada912 of +msgid "Address the server socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: 2f4d0c4bfd8246d4984d4c843510775d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:6 of +msgid "First port to bind; also the base of the allocation range." +msgstr "" + +#: 48b1e43c9f2a4e81a9c760ca0d80bad0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:8 of +msgid "Maximum concurrent clients. Defaults to 10." +msgstr "" + +#: 8786a9d2b6b449209be813778d785712 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:10 of +msgid "Step between candidate ports. Defaults to 1." +msgstr "" + +#: 15c0118df8da4454a7fea8d529989237 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:12 of +msgid "Number of ports per step. Defaults to 100." +msgstr "" + +#: 973382c423524d44a569c318399e0b99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:14 +#: d5f8f341d98d478289d9a226f37c1003 of +msgid "Concurrent file transfers allowed. Defaults to 10." +msgstr "" + +#: 4fe9a7607dbc4eebbce4e62f9306c76f +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:17 of +msgid "" +"Reserve a port range across processes, so several instances on one host " +"do not collide. Defaults to False." +msgstr "" + +#: 8aa56addc6ae4a7790dc957d1e1c2b60 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:20 of +msgid "Start the console command thread. Defaults to True." +msgstr "" + +#: 242566ff64584fba88e4fed8bb189110 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:28 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:23 +#: b6d5c80f7a8e4358889f1d90f9a579a4 of +msgid "" +"Worker slots for `submit_task` and threaded command handlers. Defaults to" +" 10." +msgstr "" + +#: 87d1cd7d6569498b8af8bc7064235784 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:26 of +msgid "" +"When True, do not call `start_TCP_Server`; the caller starts the server " +"when ready. Defaults to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:29 +#: ac43634ddb1a4d15a9f3d0f58d011441 of +msgid "" +"Negotiate the RSA-encrypted channel for every connection. Defaults to " +"True." +msgstr "" + +#: 1ffccca56902418ba98ee0c6776ad8fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:37 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:32 +#: e0eb36563dc5479a92ce7555eb5a7965 of +msgid "" +"``[pub_key_path, pvt_key_path]`` pair used instead of the default key " +"lookup; an invalid pair is ignored." +msgstr "" + +#: 420bbdf8d6bb400fbd3548bea03354ac +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:35 of +msgid "" +"Buffering ceiling in MiB for the in-memory forward pump; past it the " +"uploader is told to pause. Defaults to 2048." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 012113b84d6e439db2b772b810838e0a 57443c71b0b84d20b3cf755ee7118632 +#: 63ab5a2f2327475ba324e245f8e6e2ac 7cfe1a15e78d4c2d91a46599dc106667 +#: 8b33b297e4e64f2ba7463207ef17cc68 a4cb07de3c4d4a5c959c403938ed51e6 +#: a85e930c77364c9eaac1c8c9e9e2c740 c79723467b3a4fa1a7ddcb1038fd8414 +msgid "Raises" +msgstr "" + +#: 37d7580e55904c278f12fe0f31fe0d3d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:46 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:39 +#: ed59ec69b9a1430597e6d8758051aff5 of +msgid "" +"If the ``.Flow`` directories or ``decode_command_table.json`` cannot " +"be created or read." +msgstr "" + +#: 810bc26336b24ad1acba3f56d466f8e9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:1 of +msgid "Reserve this server's port range under the cross-process lock." +msgstr "" + +#: 5540b6efb5864c188e080ec7aad5c3ca 9e68e81f7a184af8a8fe90557dbaa5de +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:3 of +msgid "No-op unless ``is_hand_alloc_port`` is True." +msgstr "" + +#: 356ed2e8333d49b98ed6834612b4b56d 4a28dc37e6b74154b3790646ec8abfa7 +#: 5e267cb8af8d49e38ddaeabd8a7e9b57 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:6 +#: daf116d5b2264b1a88c836c1ad5192a3 of +msgid "Step between candidate ports." +msgstr "" + +#: 28bfef7616a1498db2b8827758175f68 7e663e1ae24640d1b856b1f9a0525380 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:8 +#: e18848eedc5d4d8abd97d73a0984a5d3 f81918ab77304aeb99302710e3062db2 of +msgid "Number of ports per step." +msgstr "" + +#: 11e0523d1b194702a07a92748468da25 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:1 of +msgid "Release this server's reserved port range." +msgstr "" + +#: 801c063c5fa447bda1dbf7f5ccbc6a9a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the server port range for this process." +msgstr "" + +#: 672bb7a9496c43ac8b706a3d0062ddfd +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:1 +#: of +msgid "Report whether the server port range is reserved by some process." +msgstr "" + +#: 19c63440e9ff4dadb6a3614ef17ef055 39423e052b70403ba07a713aa9e3cb4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:3 +#: of +msgid "True while the lock file exists." +msgstr "" + +#: 5b6cf828ffb24e098a0c86a09818da73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_unlock:1 +#: of +msgid "Remove the lock file that reserves the server port range." +msgstr "" + +#: 479bdf5518dc4a029ecf47ad44bdcd5d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:1 of +msgid "Allocate the next free server port range and record it on disk." +msgstr "" + +#: 34c085a1bfca4dedafc749bfdcf14ad5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:3 of +msgid "" +"``port`` is moved past the ranges already recorded by other servers, so " +"the instance ends up with a range of its own." +msgstr "" + +#: 197b30e318d741c2b1f8afc925e91e5c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:11 of +msgid "If the server port info file cannot be read or written." +msgstr "" + +#: 1c97c8ef484b42ab86d36b3742bf3879 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_free_port:1 of +msgid "Drop this server's entry from the on-disk port range record." +msgstr "" + +#: 9fc07e857fa048e2956e64bf78f6e386 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:1 of +msgid "Allocate a transfer port, waiting until one is free." +msgstr "" + +#: 505d7b9fb1024bc8ba886e5b0a38fbf2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:3 of +msgid "" +"Allocated port, or 0 when allocation is disabled " +"(``is_hand_alloc_port`` False)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:5 +#: e85fdf7f7a594e0b9aed565b4a3ab42f of +msgid "Allocated port, or 0 when allocation is disabled" +msgstr "" + +#: 153357eb91794cb692467afe5e94b41c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:6 of +msgid "(``is_hand_alloc_port`` False)." +msgstr "" + +#: 26c5c3768e1446e78c9efc5f8038b23d 280a1814abb049cbb26998bd681609ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:1 of +msgid "Release a port obtained from `palloc`." +msgstr "" + +#: 0e0379de669843778f6827973c29372e 3ee789104c4749b1a5749730be83a046 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:3 of +msgid "Port to release." +msgstr "" + +#: 6a8d4efd9f9a45e399525e2f169035ee +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:1 +#: f9203c3ebcb24fd0b5110110b8693b9c of +msgid "Allocate the next port above the base, or the first free one in range." +msgstr "" + +#: 73e9e2ddd451402a93d10dbac6ac9374 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 9d96a242eab8433ca750d670a5f63b82 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:5 of +msgid "Allocated port; None when the upward range is exhausted; 0 when" +msgstr "" + +#: 7b05e18fccbe4ab081d3cd22d59a48e0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:6 +#: ebba1d1151cc4d5991f783986cd0f480 of +msgid "allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 5f15d2ac36eb47a2b23fb93de93755c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:1 +#: f2d9abe435cf49f6825bb72a68013934 of +msgid "Release a port obtained from `file_palloc` and step the cursor back." +msgstr "" + +#: 6c1fb07a212d4bf19717407b0c7184d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:3 +#: c933ad6d68ec4c5d9bb5afc88c386fdd c9edcb78694a444fb0f2cc5f72db01fa +#: e32e165133154b9791e35f117f8a85a9 of +msgid "Port to release. Ignored when allocation is disabled." +msgstr "" + +#: 1afd81c685234a20aa41c5cde93d327d 7f5db505a5fa4c03a56e9ac5ff6aba74 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:1 of +msgid "Allocate the next port below the base, or the first free one in range." +msgstr "" + +#: 3fc63f6d51aa4c0fb22fd0f945f9cfd7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 74a441ccfa5e421dadd215ef0725a25f 9b5089cb85494c069d408e344db75d9d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:5 of +msgid "Allocated port; None when the downward range is exhausted; 0 when" +msgstr "" + +#: 183b4335eebc41058759e30b97477018 9c4b9bddd98c463f9e140bd2d8425dad +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:1 of +msgid "Release a port obtained from `spy_palloc` and step the cursor back." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:1 +#: c66c76cab8564da4b6b6cb855fd63fd5 df9eb5ed5e844a6a8f00e77be5c032e3 of +msgid "Register a custom command handler." +msgstr "" + +#: 85787fb3d66340eb9920983dd95e03c6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:3 +#: cdec2f24d2dc47bc8c505b7472dc3bf7 of +msgid "" +"Command to intercept, e.g. \"/my_command\"; matched case-insensitively " +"against the first token." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:6 +#: c7c29a3836c9493e9c8804cc5896144a of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back to the sender as the response." +msgstr "" + +#: 7c64d0c0b9ac44b995811a0fdaa0dfaf +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:10 of +msgid "" +"\"server\" for commands arriving from clients, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 7d2a7d91ad9743b29110a43100444097 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:13 +#: a0c074a9da664c5eb5fa63deb6765c36 of +msgid "" +"Run the handler on the worker pool instead of the reader thread. Defaults" +" to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:17 +#: ce0ad78e00b24bb8b675ce8d715b8323 e6c616f887334cb6925c03de2ad6f64c of +msgid "" +"False when ``where_to_run`` is neither \"server\" nor \"client\"; the" +" handler is then not registered." +msgstr "" + +#: 3a440bf70b36447e82577535a85baca1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:19 +#: f125f1386b434f3a8199b1ceb8c44eca of +msgid "False when ``where_to_run`` is neither \"server\" nor" +msgstr "" + +#: 1a68dac068db434eb76e28015005f29c 7c7784e8bc9f4a76b12b6be2d3df9285 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:20 of +msgid "\"client\"; the handler is then not registered." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:1 +#: ef5d20f1a16945368383a8cdbc3daa19 of +msgid "Register ``listener(client_id, message)`` for every inbound plain message." +msgstr "" + +#: 12cdf8300e934ace8498d00d39501ba2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:3 of +msgid "" +"Plain messages are the lines received from clients that do not start with" +" ``/``; commands go through the registered command handlers instead." +msgstr "" + +#: 9ec5fafe8ded489785400e3e489b8573 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:6 of +msgid "" +"``listener(client_id, message)`` where ``client_id`` is the sender's " +"``\"ip:port\"``. It runs on the receive thread, so it must not block, and" +" exceptions raised inside it are swallowed." +msgstr "" + +#: 3a640e40aeeb43938e65b49a2cd3dbba +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:1 +#: edb8edc2eb3f433186b54807a8ac43ad of +msgid "Unregister a listener previously added by `add_message_listener`." +msgstr "" + +#: 0336bd7428fa4f688cce21bb6a4156fe 6fd7bb75a29e4973ac2bd05119255039 +#: 977594c4b1074688958a018c03d9ee5b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:3 +#: b59375740438476aa73d6f19c2240ce6 of +msgid "Listener to remove; an unknown one is ignored." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:1 +#: cf95477e5794404fbdf4e877a132ba1c of +msgid "" +"Register ``listener(client_id, full_path, name, size, command)`` per " +"saved file." +msgstr "" + +#: 9c57d09f6d3d437890ae3ad306071701 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:3 of +msgid "" +"Fired after a file uploaded by a client (a direct send, or a forwarded " +"file/folder item staged on the server) has been fully written to " +"``file_transfer_dir``." +msgstr "" + +#: 6c938b7c8be3464f9925527b6deb4e1d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:7 of +msgid "" +"``listener(client_id, full_path, name, size, command)``; ``client_id`` is" +" the uploader's ``\"ip:port\"`` and ``command`` the wire command that " +"triggered the transfer, so a listener can recognise protocol pushes such " +"as ``/crypto_pub_key``. It runs on the transfer thread, so it must not " +"block." +msgstr "" + +#: 549734db517346ec814fa83ec2f46f00 57b1e98125974187b986386da7991dd0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:1 of +msgid "Unregister a listener previously added by `add_file_listener`." +msgstr "" + +#: 58f1ccd078ea406dab00d6d4be886a76 7a6c25f715d14b0aaf4ec7009db9fc3c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:1 of +msgid "Run a callable on the instance's worker pool." +msgstr "" + +#: 6e05c7f46b244ae195c32ee52797fa37 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:3 +#: a51df9d8fc924d468d8f6d8a61dfff12 of +msgid "Callable to run." +msgstr "" + +#: 6f9eee14b6cb4daf82acfbba1e34e873 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:5 +#: e1c79706c9414895bdf7bf183bf5d41a of +msgid "Positional arguments forwarded to ``func``." +msgstr "" + +#: 9221436fa83a4831942cf5be4e37e2cb 993bdb8fbe3240d3b1032014576182fe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:7 of +msgid "Keyword arguments forwarded to ``func``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:10 +#: a6b26d6cf54d4fafac2950cbe33ba56f df538e45ae7f4bd19398fd3d063a4744 of +msgid "" +"Handle for the submitted call; its worker slot is released when the " +"call finishes." +msgstr "" + +#: 214ffa9d5cb242d1b7a5f76c0da16cd6 60f19983f7384aefbb6c97b84dd25f93 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:12 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:12 of +msgid "Handle for the submitted call; its worker" +msgstr "" + +#: 942e2e90fc904344b71b0c2d9c39c9f6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:13 +#: eadf6af0a0954798933dc427ba8e107e of +msgid "slot is released when the call finishes." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:1 +#: a2284b241a28498892e5a59158961b2e fece61298a354d6db119309b29c6751d of +msgid "Start a temporary listener for a side channel (not the main protocol)." +msgstr "" + +#: 01d42ba32c2b41f482dd077cfb950f9b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:3 +#: b7455f1055b7459ca8eee2b5052eb808 of +msgid "" +"``handler(client_socket, address)`` started in its own thread for every " +"accepted connection." +msgstr "" + +#: 5e1b84445ee34c9cac00e1a2c9ef297b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:6 +#: c76bc24facf8407babf9023ef19c3879 of +msgid "Port to bind; None allocates one with `palloc`." +msgstr "" + +#: 4855e3ca5a3a41dfa37eb32a8d6c8d9c 9c69c4dd1d904078af48c6308ba35895 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:8 of +msgid "Listen backlog. Defaults to 1." +msgstr "" + +#: 29148f4a3160460f8dc27fd1e64c0a31 47f98fd15e6144bd845d175cd039a6c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:11 of +msgid "" +"``(port, thread, stop_event)``; setting ``stop_event`` ends the loop," +" which closes the socket and frees the port." +msgstr "" + +#: 91f803f6aa944d07a57cff61d1071efe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:13 +#: ecf3d32c44db40a8a13545d623ffe2c9 of +msgid "``(port, thread, stop_event)``; setting ``stop_event`` ends the" +msgstr "" + +#: 6ca9f2a625634468a15171916ec6d6ea +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:14 +#: abce9546b7444c6383c9020c4133ec63 of +msgid "loop, which closes the socket and frees the port." +msgstr "" + +#: 04767fd6d11641ac88c41c78867609f5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:17 +#: a270cfabdac64296b8a33ad4d662896f of +msgid "If ``port`` is None and no port can be allocated." +msgstr "" + +#: 031d3e96394c454093561375a377dcce 7e534c863e4a4275adf9a3a93dd5cbc0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:1 of +msgid "Open a temporary outbound connection for a side channel." +msgstr "" + +#: 112fda54e1eb4981b4272df61c5416e2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:3 +#: f9f4af8e25654b4ca38700b9faf635ef of +msgid "Host to connect to." +msgstr "" + +#: 26edcce3a1c24d5d95ca0b2a86cdff36 59d3c414b4304673be5d3232d2291680 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:5 of +msgid "Port to connect to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:7 +#: c5766672f68f47c9908e68d2ed3be7f3 of +msgid "Local port to bind; None lets the OS choose." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:9 +#: bfc1b713e9fc4fe797e50c357f472e76 c0e20701686a4ea68dcce54795822508 of +msgid "``on_data(data, client_socket)`` called for every received chunk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:13 +#: c563d04fcd7a41f1a2aeace42974430a cbd0df6f67a64a5580227be38a10020a of +msgid "" +"``(client_socket, thread, stop_event)``; setting ``stop_event`` ends " +"the receiver thread." +msgstr "" + +#: 0e1b9679f3104a9caab601e6bc7c0905 4fbec161e0c84ef1ba95379314846158 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:16 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:15 of +msgid "``(client_socket, thread, stop_event)``; setting ``stop_event``" +msgstr "" + +#: 14528f9f11144ced9b5822d2640fe2a9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:16 +#: c76fce92c3f944c1857df89bbc134540 of +msgid "ends the receiver thread." +msgstr "" + +#: 5ea5fd66c3d3434292e4d8e1dd1da094 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:1 of +msgid "Send one message to every connected client." +msgstr "" + +#: 32945f04e0564f9d88495506f0ab4f90 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:3 of +msgid "Clients whose send fails are disconnected and removed from ``clients``." +msgstr "" + +#: 00de0b1f60b14189b1629f8146db68a6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:5 of +msgid "Payload passed to `send_message`." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:7 +#: a91c8f6921c7435e990516c2134efcf8 of +msgid "``(ip, port)`` to leave out, typically the client the message came from." +msgstr "" + +#: 9d57c62f1fd94c1dacfe32aa50cc9665 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:1 +#: of +msgid "Send the messages of a console line to the clients named in it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:3 +#: e7fbf4bbd84b4920a2b9591fabcaf2d9 of +msgid "" +"``/send_msg`` line as typed: message text followed by one or more ``(ip, " +"port)`` identifiers; each message is delivered to the identifiers that " +"follow it. Addresses that are not connected are skipped with a console " +"notice." +msgstr "" + +#: 9d4819fe4ca94a1b81c9e0423ff98212 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:1 of +msgid "Write one line to a client socket, encrypting when the channel is up." +msgstr "" + +#: 1c7b88e29a0143a39e13441769186ef4 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:3 of +msgid "Target connection." +msgstr "" + +#: 2d32d96ad70e41d893b15bfa90c0864b 6f8bd9adaf2b4c44a9446b049144fe21 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:5 of +msgid "" +"Payload; a str is stripped and newline terminated, bytes are sent as they" +" are." +msgstr "" + +#: 5c03983cb460497fb72f126c1d8a2bc1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:9 of +msgid "" +"True when the payload was written, False for an unsupported payload " +"type." +msgstr "" + +#: 0b11a149117c4a1fb6bcf2fba3d1a515 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:11 of +msgid "True when the payload was written, False for an unsupported" +msgstr "" + +#: 602950a9f5ad48ea92ab60aea3b26488 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:12 of +msgid "payload type." +msgstr "" + +#: 149f767d72dc4da7b1d88fed768d2474 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:15 of +msgid "If the server is not running or no socket was passed." +msgstr "" + +#: 07dac2d20d7041779c8b189d45ee2e64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:16 of +msgid "If the socket write fails (the original error is re-raised)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:1 +#: caf2377ee7f348ab8c95bf2a27c44be4 of +msgid "Read up to ``msg_length`` bytes from a client socket." +msgstr "" + +#: 214af2667d0f443eb16c0a4aaf723b2e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:3 +#: cf94d4a352744a61a1f781128fae9694 of +msgid "Connection to read from." +msgstr "" + +#: 2b0cd669c9994324be7570e59a04677d 7c3eb31687804ba5a59acc140ba50230 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:5 of +msgid "Maximum number of bytes to read." +msgstr "" + +#: 51eade0145d74a2bbfc51070bc779703 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:8 +#: ad9dfdd3ef56445181d7204b7b5bfa15 of +msgid "Received bytes, empty when the peer closed the connection." +msgstr "" + +#: 7103cfcec26c44999cb5464251ee461c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:1 of +msgid "Serve one accepted client until it disconnects." +msgstr "" + +#: 65f9de7f44774aa6aef81fcbe4dc1c64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:3 of +msgid "" +"Registers the client, greets it, announces the encryption mode and reads " +"lines until the peer closes: commands go to `handle_command`, plain " +"messages go to the message listeners and to ``messages_dict``. Runs in " +"its own thread; the client is removed from ``clients`` and the socket " +"closed when the read loop ends for any reason." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:9 +#: db3915757e2842c8afee97b2d471411e of +msgid "Accepted connection." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:11 +#: fae378e888be4821a14ed2483cfde1e0 of +msgid "Peer ``(ip, port)``; used as the client id and as the key in ``clients``." +msgstr "" + +#: 32886e2babcf4283924034c600453c4a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:1 of +msgid "Dispatch one command line received from a client." +msgstr "" + +#: 8972b8c4edf149fd9a4008832c85ed31 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:3 of +msgid "" +"Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, " +"``/crypto_mode``, ``/file``, ``/file_folder``, " +"``/server_file_transfer_port`` and the crypto exchange lines) are handled" +" here; any other name goes to the handlers registered for the \"server\" " +"side via `register_command`. An encryption-mode mismatch closes the " +"connection; an unknown command is only reported on the console." +msgstr "" + +#: 208294924333477a8e7d65aa3130777e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:10 of +msgid "Connection the line came from." +msgstr "" + +#: 306e5026cfc1441d8c754f0e02866cf3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:12 of +msgid "Peer ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:14 +#: a2a3a1fd57e14260a53675457fdff0b4 d87b7a9e04464b7ea4f979d46b42932b of +msgid "Line including its leading ``/``." +msgstr "" + +#: 8d727a2aaab14beaa1b6858b59b0821d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:17 of +msgid "" +"Response for that client, or None when no response is due (crypto " +"lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:19 +#: ed04a3bf285b4f57bb6a5de0c4f7284f of +msgid "Response for that client, or None when no response is due" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:20 +#: c4ca8e9c3e9941e1b33b5e0af2c13bab of +msgid "" +"(crypto lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:1 +#: a784dfdc5ded411d985426e36f6e5435 of +msgid "Send one plain message to a connected target, tagged with its origin." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:3 +#: b545977244dc402aa8586264d2d933f3 of +msgid "" +"Public API for forward extensions: the message is wrapped in a " +"``/send_msg_from `` envelope so the receiver can " +"attribute it to the originator (see `parse_forwarded_message`)." +msgstr "" + +#: 8ee5275fbf894581ade9e6f30d5d4aff +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:7 +#: ee97948978e64b099947a0716f27d581 of +msgid "Destination ``(ip, port)``." +msgstr "" + +#: 38ffd8a7079d4794811e3504c01052f8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:9 of +msgid "Payload to deliver." +msgstr "" + +#: 3b48d6e3b84b4dde9a09f3c2f07c8b61 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:15 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:13 +#: cf0f9590c44e40309471c70ebd2e433d e2207f0b24bf42c984505e0de8c8f8bf of +msgid "Originating client ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:14 +#: d2471a90e269464c977239f406051120 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the envelope was sent." +msgstr "" + +#: 1ae8a7060df7423895175f210441a399 320d99b8923f4eaa886ffe0a16bfc6d9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:24 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:16 of +msgid "False when ``target`` is not connected (a console notice is" +msgstr "" + +#: 1a97f59b270f404096d9c2b37ab09a8c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:17 of +msgid "printed); True when the envelope was sent." +msgstr "" + +#: 6baab06dc12e4ecc80d1a16c26c2a19a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:1 of +msgid "Build the tagged wire command that pushes one forwarded item." +msgstr "" + +#: 0c3f9dcbcf8f413fbd5befefb960c402 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:3 of +msgid "" +"Public API for forward extensions. The originator tuple sits before the " +"trailing transfer id, where the receiver's existing parsers ignore it and" +" `parse_forward_originator` recovers it for attribution." +msgstr "" + +#: 3cf58937a2d04172bf544119279d85c1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:7 +#: a5ccb57e63d144c2844910f8ecf2faa4 of +msgid "\"file\" or \"file_folder\"." +msgstr "" + +#: 621a93603c8549c0ba047bee6aae5a38 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:9 +#: a20ab65e584c4daf9a9153008b713ee1 of +msgid "Relative folder path (folders only)." +msgstr "" + +#: 410039575afd4797a0eb1be9098ccda5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:11 +#: f0f56fb642874c3889fb0dacfed4f4ef of +msgid "File or folder name." +msgstr "" + +#: 214d3e0a88fa41d8a02af0a5d09c5ac6 72d2b6fdd324482eaea9803def1fe2da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:15 of +msgid "Transfer id shared by the pushed item." +msgstr "" + +#: 0aa1bd5b053544e98ae4ab3ae1add5f7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:17 +#: e7826b64fe174bf0a6f7faa4df49f491 of +msgid "Receiver-side destination directory." +msgstr "" + +#: 93e666db45e745619b90c744e1646b73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:20 of +msgid "Command line to hand to `send_message`." +msgstr "" + +#: 56d8327bb7414a5e85292f8f2ace9d42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:1 of +msgid "Push one forwarded file or folder item to a connected target." +msgstr "" + +#: 3098afa7f4ac43a184d081dda8faa6ba +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:3 of +msgid "" +"Public API for forward extensions: sends the line built by " +"`forward_target_command`, which the receiver attributes with " +"`parse_forward_originator`." +msgstr "" + +#: 9b9634061040430484cd2596f43a8feb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:22 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:25 +#: b8bb8c432f7649d5a35480fec0509a51 of +msgid "printed); True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:1 +#: b03f4edeca5f4cc19f6d695aac3690de of +msgid "Bind the server socket, then accept clients until `stop` runs." +msgstr "" + +#: 7e027ea659ab48e19ea346cb284fe144 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:3 of +msgid "" +"Blocks the calling thread. A console command thread is started when " +"``is_input_command_in_console`` is True, every accepted connection gets " +"its own `handle_client` thread, and a client beyond ``max_clients`` is " +"refused with a message. Socket errors and the end of the accept loop both" +" end in `stop`." +msgstr "" + +#: 291f5a8cc4f648ee9910e5cd45074675 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:1 of +msgid "Read console commands until the server stops." +msgstr "" + +#: 1ba2e428d432496688ef23f1c07562da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:3 of +msgid "" +"Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, " +"``/file_folder``, ``/multiple_file_multiple_client``, " +"``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward " +"commands are client-only and are refused here. Any other name goes to the" +" handlers registered with ``where_to_run=\"client\"``. Ctrl-C and EOF " +"stop the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:1 +#: eb90f49c77e64006a0c9ae861ff849b1 of +msgid "Stop the server and release everything it owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:3 +#: a059b1e71106487999c83c2e2042f509 of +msgid "" +"Closes the server socket and every client connection, flushes the message" +" and event stores, releases the allocated port range and clears " +"``running``. Safe to call more than once." +msgstr "" + +#: 32b06ea315b3404caa3a05c4aed0ce64 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:1 of +msgid "" +"TCP client: connect to a server, dispatch commands, send and receive " +"messages." +msgstr "" + +#: 2b30f5c9ecd24fc89cdcd58000ee1e99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:3 of +msgid "" +"Lines received from the server go through `receive_messages`: a line " +"starting with ``/`` is handled by `handle_server_command` (protocol " +"commands plus the handlers registered for the \"server\" side), any other" +" line is a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``. With " +"``is_input_command_in_console`` the console thread `interactive_mode` " +"sends typed lines to the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:12 +#: b9532c86786c43d5aea58b24157fefed of +msgid "Server address this client connects to." +msgstr "" + +#: 2cfa1e3e1674407ba1e2683e76fef65d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:18 of +msgid "Server port this client connects to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:24 +#: af04fdee2c95431a9b0c17f96e50c18c of +msgid "Local address the socket binds to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:30 +#: b1d4b5606e1c48b6a915815527542758 of +msgid "Local port, None when the OS chose one." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:32 +#: d926b4f3eb4b40a4acb21f780a4dc227 of +msgid "int | None" +msgstr "" + +#: 500cfd177cf5453d887f6904b73b7851 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:36 of +msgid "True while the connection is up." +msgstr "" + +#: 40f2a9e1b730422db3dd58bc2a6046e3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:1 of +msgid "Create the client and, unless extended, connect and start reading." +msgstr "" + +#: 0552aa36b7b74474af8f43b380dbee54 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:3 of +msgid "Server address to connect to; required before `connect` is called." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:6 +#: d246ff9aaf154bfa91b5c4c9d293afd5 of +msgid "Local address the socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:9 +#: e981b68ebbea46e9b556b09f4b5cd44a of +msgid "Server port. Defaults to 65432." +msgstr "" + +#: 766bf4c138234fe081bbd2668dcb2a5d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:11 of +msgid "Local port to bind; None lets the OS choose an ephemeral port." +msgstr "" + +#: 5d661c0f91f64ef39cacb1a5466d752c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:14 of +msgid "" +"Socket timeout in seconds for connect and receive. Must be None when " +"``is_wait_server`` is True." +msgstr "" + +#: 9478d02062284ecaa7838f931ab80780 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:17 of +msgid "Step between candidate ports in the allocation range. Defaults to 1." +msgstr "" + +#: 2e1695ae510b43758793257124fd23c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:22 of +msgid "Enter interactive mode after connecting. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:25 +#: e02da86957dd4f45bf84ede4e9c82cda of +msgid "Keep retrying while the server is not reachable. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:31 +#: e1fb7f67e1714a61a9e8fe500555c700 of +msgid "" +"When True, do not call `start_TCP_client`; the caller connects when " +"ready. Defaults to False." +msgstr "" + +#: 1634dbe84a95421a92a660f7c6fdd10e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:34 of +msgid "Negotiate the RSA-encrypted channel with the server. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:40 +#: cff66a7ac1ad410f96ed1ee03cb1cf53 of +msgid "" +"Buffer ceiling in MiB, kept for parity with the server class; the " +"client's forward path does not read it today. Defaults to 2048." +msgstr "" + +#: 0f06ff3b6db940da96ec09bee3dba526 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:45 of +msgid "If ``is_wait_server`` is True and ``timeout`` is not None." +msgstr "" + +#: 5dd677b9627d4b84b53dc7a6b2848b3f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:6 of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back as the response." +msgstr "" + +#: 0a57a7875bb94767904ff8d93fa77eb8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:10 of +msgid "" +"\"server\" for commands pushed by the server, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 876e8fe2a6d645829ba12ddbad5006df +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:1 of +msgid "Register ``listener(sender_id, message)`` for every inbound plain message." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:3 +#: d166a2cc8ea04bed82c5f6dd61dcdcf6 of +msgid "Mirrors the server-side contract; commands are not reported here." +msgstr "" + +#: 412834182c9442adaf0b85e24c200ccc +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:5 of +msgid "" +"``listener(sender_id, message)``; ``sender_id`` is the author's " +"``\"ip:port\"`` — the forwarding client for a message another client " +"forwarded here (``/send_msg_from`` envelope), or None for a direct push " +"from the server, which names no client author. It runs on the receive " +"thread, so it must not block, and exceptions raised inside it are " +"swallowed." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:1 +#: fc8a83e0b5f84273a8287b122c1a9a3e of +msgid "" +"Register ``listener(full_path, name, size, command)`` per saved inbound " +"file." +msgstr "" + +#: 34e3386853564aab9a824e96438fc3e5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:3 of +msgid "" +"Fired after a file pushed by the server (a direct send, or a forwarded " +"file/folder item) has been fully written to ``file_transfer_dir``." +msgstr "" + +#: 63f925709b83433e967610672b6fbc79 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:6 of +msgid "" +"``listener(full_path, name, size, command)``; ``command`` is the wire " +"command that triggered the transfer, so a listener can recognise protocol" +" pushes such as ``/crypto_pub_key``. It runs on the transfer thread, so " +"it must not block." +msgstr "" + +#: 0e91f82d8856443497c399f97643b757 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:1 of +msgid "Reserve this client's port range under the cross-process lock." +msgstr "" + +#: 6029a28126344e45b24394cb56b2f8f8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:3 of +msgid "No-op until the server assigns a range (see ``/client_alloc_port_range``)." +msgstr "" + +#: 32bed401c3574959ba5ed08fc9078401 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:1 of +msgid "Release this client's reserved port range." +msgstr "" + +#: 9f86b7aac4f44886be9cd87b56072b63 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:3 of +msgid "No-op unless a range was assigned (``is_hand_alloc_port`` True)." +msgstr "" + +#: 8154f96d9ec74bd588971a806d42f8c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the client port range for this process." +msgstr "" + +#: 69c2dd5d5d1d497c8bac1e62ab4fc30a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:1 +#: of +msgid "Report whether the client port range is reserved by some process." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_unlock:1 +#: c88f37ef2d5c47bda346e804508d6ec0 of +msgid "Remove the lock file that reserves the client port range." +msgstr "" + +#: 478443e187814a6ca7ad798277db023f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:1 of +msgid "Allocate the next free client port range and record it on disk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:3 +#: a8736540f4c3402fad9508e619d559d7 of +msgid "" +"``port`` is moved past the ranges already recorded by other clients on " +"this host, so each instance ends up with a range of its own." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:11 +#: bc44b1604e02467280b59cb8ad63d0af of +msgid "If the client port info file cannot be read or written." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_free_port:1 +#: e48dff576a074109a135db168b5dbe98 of +msgid "Drop this client's entry from the on-disk port range record." +msgstr "" + +#: 3aa62e8843c6495c862f43fc6dda9d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:1 of +msgid "Allocate a port, waiting until one is free." +msgstr "" + +#: 2f7752fa51d946b1a4a566b38f046e80 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:3 of +msgid "Allocated port, or 0 when no allocation range was assigned." +msgstr "" + +#: 503ab40fc32c4d548f65fa3272b2f4d2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:5 +#: b9fd549c95f24c2080d87cec92514db6 of +msgid "Allocated port; None when the upward range is exhausted; 0 when no" +msgstr "" + +#: 151a65f1c1984e4196c750108d0611cb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:6 of +msgid "allocation range was assigned." +msgstr "" + +#: 0c6201bb2d7e45d3a4916d7e50a21e10 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: 76c766a9c61f4075a762ccf55038a65e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:6 of +msgid "no allocation range was assigned." +msgstr "" + +#: 8d500513e8854ba4bb91c8905a9adb73 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:7 of +msgid "Local port to bind; None allocates one with `palloc`." +msgstr "" + +#: 840360623c8a4c2bbafe8fe7bbf9209c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:1 of +msgid "Connect to the server and start reading from it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:3 +#: df94288e660b48f6809358b3dc234ec9 of +msgid "" +"Binds ``client_port`` when one was configured, then retries while " +"``is_wait_server`` is True and the server is not reachable yet. Once the " +"socket is up the receive thread is started and the encryption mode is " +"negotiated, which closes the connection when the two sides disagree." +msgstr "" + +#: 22fccba0ab36499a9a7f4427abe5e9f4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:8 of +msgid "" +"True when the connection is established (and, if encryption is " +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 0270d0a2d42740a9889106339d9f2dbb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:10 of +msgid "True when the connection is established (and, if encryption is" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:11 +#: c194691cedfb48c6bfd97d9bdc6f2246 of +msgid "" +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 4dc38e3ef3b74b8e8c27378242a1925a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:1 of +msgid "Read from the server until the connection ends." +msgstr "" + +#: 431649a0d703476dadc6afa8adbe53d4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:3 of +msgid "" +"Runs on the receive thread: plain lines are reported to the message " +"listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes " +"are attributed to their sender first), other ``/`` lines go to " +"`handle_server_command`. Any end of the connection clears ``running`` and" +" releases the port range." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:1 +#: cf284b59f7f849039404ced20f8e12db of +msgid "Write one line to a socket, encrypting when the channel is up." +msgstr "" + +#: 40ba2ede2d3f4666b82107af129d24d0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:3 of +msgid "Target connection; the client passes ``self.client_socket``." +msgstr "" + +#: 2e663941eaf142cb96738add05f9ef4d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:10 of +msgid "" +"True when the payload was written; False when the client is not " +"running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: 38905ec7539d42e0839907c32dd8667f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:12 of +msgid "True when the payload was written; False when the client is not" +msgstr "" + +#: 3f7e2e6fedda476f996132c3f677f2f0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:13 of +msgid "running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:1 +#: a9220a130e694e0aba1d3b6320dff498 of +msgid "Send the payload of a console line to the server." +msgstr "" + +#: 2e208e83a89e4dac9793460a8808a042 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:3 of +msgid "" +"Console line such as ``/send_msg hello``; the first token (the command " +"name) is dropped and the second one is sent." +msgstr "" + +#: 39c8e70ca92b43b8ac687bc0aa7f4073 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:7 of +msgid "If the line has fewer than two tokens." +msgstr "" + +#: 8d47f30ae7b0451f8ecfe207ec835ff3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:1 of +msgid "Read up to ``msg_length`` bytes from a socket." +msgstr "" + +#: 10a39005f45c46728ef9a000eeaf9109 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:1 of +msgid "Dispatch one command line pushed by the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:3 +#: a7e781fde939475cb081204adac16ded of +msgid "" +"Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the" +" connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange " +"lines, and the transfer lines ``/file``, ``/file_folder``, " +"``/forward_upload``, ``/pause_trans``, ``/start_trans``, " +"``/forward_error``. Any other name goes to the handlers registered for " +"the \"server\" side via `register_command`; an unknown command is only " +"reported on the console." +msgstr "" + +#: 91942d88960c4793b5eab8c3654c1400 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:1 of +msgid "Forward plain messages to other connected clients through the server." +msgstr "" + +#: 75bb9147a7464a278e19af9d03b04ecb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:3 of +msgid "" +"The console command ``/forward_send_msg`` uses this; the client must be " +"connected. The server wraps each message in a ``/send_msg_from`` envelope" +" so the receiving client can attribute it back to this one." +msgstr "" + +#: 0f3c201936df45b687dc5532531b4a4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:7 of +msgid "Message texts to forward." +msgstr "" + +#: 277bbcb2143943b5b983badb00c3f4fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:9 of +msgid "Destination ``(ip, port)`` tuples." +msgstr "" + +#: 5ea8eea489fc43108a439d54fba31068 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:12 of +msgid "" +"True when the request was written to the server; False when the " +"client is not connected." +msgstr "" + +#: 2bc5d7e5dfa94eb29b49f12562f57c88 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:14 of +msgid "True when the request was written to the server; False when the" +msgstr "" + +#: 45d974ad946b408b91d41bcce65abcc8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:15 of +msgid "client is not connected." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:1 +#: c9401da7c37e40e5978c80e4876b11a0 of +msgid "Read console lines and act on them until the client stops." +msgstr "" + +#: 793bcfd02c014200b0ec47f8e15b4d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:3 of +msgid "" +"``/quit`` closes the connection; ``/send_msg``, ``/file``, " +"``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, " +"``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are " +"handled locally; any other name goes to the handlers registered with " +"``where_to_run=\"client\"``, and anything left is sent to the server as " +"it stands. Ctrl-C and EOF close the connection." +msgstr "" + +#: 6dbae803a56c442588b28af664b7a0e9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_file_console:1 of +msgid "" +"/forward_file ... ... [dest] (client " +"only)." +msgstr "" + +#: 05cad8b03e0747dc804ffbfda3122fe1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_folder_console:1 of +msgid "" +"/forward_folder ... ... [dest] " +"(client only)." +msgstr "" + +#: 6e9a9717a782419baecca63fd1f46baf +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:1 of +msgid "Close the connection and release everything the client owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:3 +#: ba6c5badecf6465db6036735870ed305 of +msgid "" +"Stops the receive loop, releases the port range, flushes the message and " +"event stores and closes the socket. Safe to call more than once." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:1 +#: c91a3245b93f46abbb49dbef7bcfeb1c of +msgid "Connect to the server and start the client loop." +msgstr "" + +#: 0c83552867484ea780fcff8d34e5c2d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:3 of +msgid "" +"Enters `interactive_mode` when ``is_input_command_in_console`` is True, " +"otherwise keeps the process alive while the connection is up. Exits the " +"process with status 1 when the connection cannot be established; Ctrl-C " +"and the end of the connection both run `close`." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po new file mode 100644 index 0000000..e0e069b --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_udp.rst:2 +#: c777b065237a4eb1993c041388639a1d +msgid "PyFlow.network\\_api.connect\\_udp module" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.po new file mode 100644 index 0000000..cfd27ad --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rst:2 9abd125493904de2bb64c9158a11243f +msgid "PyFlow.network\\_api package" +msgstr "" + +#: ../../api/PyFlow.network_api.rst:10 7012029060714904ba5d281c1d607be9 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po new file mode 100644 index 0000000..964c4c0 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po @@ -0,0 +1,232 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rsa_crypto.rst:2 +#: def9740b5a2e4aaeb2aeed8b4f02c0ec +msgid "PyFlow.network\\_api.rsa\\_crypto module" +msgstr "" + +#: 46d3090efafd4504870cf72a4bea22ab PyFlow.network_api.rsa_crypto:1 of +msgid "crypto_api (C/OpenSSL) RSA integration for PyFlow's TCP layer." +msgstr "" + +#: 5b1ff4a00b0f46258cda2e7440368de1 PyFlow.network_api.rsa_crypto:3 of +msgid "" +"A thin ctypes binding to the shared ``libcrypto_api`` plus the key " +"lifecycle required by the encrypted TCP channel:" +msgstr "" + +#: PyFlow.network_api.rsa_crypto:6 a5a0d6d600604683990a92555a49a3fe of +msgid "" +"Reuse an existing RSA keypair from ``~/.ssh`` (PEM private key) when one " +"is present and parseable, otherwise generate a fresh keypair into " +"``.Flow/pvt_key``. A caller-supplied keypair (``custom_keys``) is " +"honoured when both files parse and the pair matches." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:10 bb413dc908624589a3e57f8a2c702728 of +msgid "" +"Anti-MITM identity check (TOFU): every connection exchanges public keys " +"in plaintext. Each side records the peer in " +"``.Flow/pub_key/pub_key.json`` under the peer's ``(ip, port)`` with the " +"SHA-256 of its public key; a later connection from the same endpoint " +"presenting a different key is rejected, and a known key seen from a new " +"endpoint is re-registered under the new ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:16 aec1a7998276428881efcfac6fe4cebe of +msgid "" +"RSA-OAEP encrypt/decrypt with the ``_VALID`` plaintext signature so a " +"stale key (for example a rotated ``~/.ssh`` pair) is detected and the " +"peers re-exchange their public keys." +msgstr "" + +#: 60edbdead17c422782e688ab68f8a6d1 PyFlow.network_api.rsa_crypto:20 of +msgid "" +"The C library must be built first (``cmake -S . -B build && cmake --build" +" build``); see ``load_library`` for the search paths." +msgstr "" + +#: 67af7d5c5f41474fba915352e506a8b4 +#: PyFlow.network_api.rsa_crypto.CryptoLibraryError:1 of +msgid "Raised when the shared libcrypto_api cannot be loaded." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaKey:1 e620122ff0bc48dd985c4b6a0b3b41ce of +msgid "Owns an ``pf_rsa_key_t*`` handle; frees it on GC." +msgstr "" + +#: 1a7d8e9db5a3435486d716b13f2a2a56 +#: PyFlow.network_api.rsa_crypto.load_library:1 of +msgid "Locate and load the shared crypto_api library (cached)." +msgstr "" + +#: 670f4a719bf24c24b48ecb5d73b373ce +#: PyFlow.network_api.rsa_crypto.get_local_mac:1 of +msgid "Return a stable 48-bit machine identifier as colon-separated hex." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.get_local_mac:3 +#: f1306bb875d34b558f7418278fded413 of +msgid "" +"Uses ``uuid.getnode()`` (the real hardware MAC when one is available). " +"Server and client on the same host share this value; the ``_`` " +"prefix in the key file names keeps them apart." +msgstr "" + +#: 333763d798c54d52a9a56a3e4e3e2155 PyFlow.network_api.rsa_crypto.RsaCrypto:1 +#: of +msgid "Key lifecycle plus RSA-OAEP encrypt/decrypt for one role." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto:3 f52515121ac74575aaa36d45dd0341a2 +#: of +msgid "" +"``role`` is ``\"server\"`` or ``\"client\"`` and is used to name the " +"locally generated keypair (``pvt_key/_priv.pem``) and the peer key " +"cache (``pub_key/__.pem``). Peer identity is tracked" +" in ``pub_key/pub_key.json`` (TOFU, see ``verify_peer_pub``)." +msgstr "" + +#: 56fed5bcfe194b7f90a0c662ae36d6c0 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:1 of +msgid "Create the crypto wrapper for ``role`` (\"server\" or \"client\")." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:3 +#: ca4e27664ccf40ce935d991db43f7c69 of +msgid "" +"``custom_keys`` may be a ``[pub_key_path, pvt_key_path]`` pair to use a " +"user-supplied RSA keypair instead of the default lookup (``~/.ssh`` / " +"generated). The pair is validated on first use (paths exist, files parse," +" the keys match); an invalid pair is ignored and the default lookup is " +"used instead." +msgstr "" + +#: 7d89122ff0f54090b67209e3b08ae29c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:1 of +msgid "Load the RSA keypair (see module docstring) and cache handles." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:3 +#: d5e494e00be44891b33cb8e0ecfb8081 of +msgid "" +"Runs under ``_key_lock``: the private-key handle must never be replaced " +"(or freed on GC) while another thread is decrypting." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.reload_own_key:1 +#: f6f7d6ec8837464dad2dc647e6141a99 of +msgid "Re-read the private key (e.g. after a ~/.ssh rotation)." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:1 +#: e95d18da34d14376987a62f6cbbec778 of +msgid "" +"Path of the exchanged public key file for ``peer_role`` at ``(peer_ip, " +"peer_port)``." +msgstr "" + +#: 52dc6fb74f0c4175bfa2a2acc23cda0d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:4 of +msgid "" +"The IP is sanitized for the filesystem (``:`` -> ``_`` so IPv6 literals " +"are safe on every platform, including Windows)." +msgstr "" + +#: 731d92a1b64342dfbf7c467af0a4c00e +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:1 of +msgid "TOFU check-and-record for a peer public key." +msgstr "" + +#: 634283c14fb44424bf148a34d156fb71 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:3 of +msgid "" +"``peer_pem`` is the PEM text received on this connection, ``(peer_ip, " +"peer_port)`` the endpoint it came from. Returns ``(ok, reason)``:" +msgstr "" + +#: 6bb58c88f8454d7daca92b5a478ae788 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:7 of +msgid "" +"key already registered under any endpoint -> accept, and re-register it " +"under the current endpoint when it moved (IPs are dynamic and ports are " +"user-changeable);" +msgstr "" + +#: 74e2bcc89bae4fe5aecf99e6208e21d1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:10 of +msgid "" +"key unknown but the endpoint already holds a *different* key -> reject (a" +" trusted endpoint suddenly presenting a new key);" +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:12 +#: f856b875adc34f4a80c2b21cd506b143 of +msgid "" +"key and endpoint both unknown -> accept and record (first connection is " +"trusted, TOFU)." +msgstr "" + +#: 556fdf46d6c543e0ab06e2d4fabad7f1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:1 of +msgid "Move a freshly received public key file into the key cache." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:3 +#: a47e84f5d21949bf98e24834f18bed76 of +msgid "" +"Idempotent under concurrency: several transfers may deliver the same peer" +" key at once (multi-connection handshakes, several client processes " +"sharing one ``received_files/`` directory); if the source is already gone" +" because a concurrent store moved it, success is assumed when the " +"destination is in place." +msgstr "" + +#: 135944d28da54948862e515eb828072d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:1 of +msgid "Encrypt ``plaintext`` with the peer's public key file." +msgstr "" + +#: 4b4dded0ad904ce2bb87c9a43c8f87e5 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:3 of +msgid "" +"Returns the ASCII wire body (no trailing newline): each chunk is RSA-OAEP" +" encrypted and base64 encoded, chunks joined with ``|``. Raises if no " +"peer key is stored at ``peer_pem_path`` yet. The whole encryption runs " +"under ``_peer_pub_cache_lock`` so the peer handle cannot be freed mid-" +"encrypt (no-GIL safe)." +msgstr "" + +#: 8f1687f4ab984c029859fd1f9cfb968c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:1 of +msgid "Decrypt a wire body with our private key." +msgstr "" + +#: 8a1d1504bf4248fa9e1288776d19810b +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:3 of +msgid "" +"Returns ``(True, plaintext)`` on success, or ``(False, None)`` when the " +"key is stale/wrong or the ``_VALID`` signature is missing. Runs under " +"``_key_lock`` so the handle cannot be freed by a concurrent " +"``reload_own_key`` (no-GIL safe)." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.po new file mode 100644 index 0000000..7393568 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.rst:2 738ded08e11742acb4854652f885aa13 +msgid "PyFlow package" +msgstr "" + +#: ../../api/PyFlow.rst:10 8a3a28b0487c4bacb81f0afaa1b2902e +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.rst:19 28ca6a63167d49798f21d2796d6acf1e +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.po new file mode 100644 index 0000000..e64a949 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.rst:2 e8518825f60e48f391b84d3fb415bb36 +msgid "PyFlow.transfer\\_web package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:10 8d8e622c8bdd4618b0e33d94e1000e42 +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:19 541ef21ed94743f2b9e11aadbde918b4 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po new file mode 100644 index 0000000..f97b443 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_client.rst:2 +#: 09f60d1b11ac4e92b480fa6284489970 +msgid "PyFlow.transfer\\_web.setup\\_client module" +msgstr "" + +#: 2f786e174934474a93c6498878a68612 PyFlow.transfer_web.setup_client:1 of +msgid "PyFlow TCP client web launcher." +msgstr "" + +#: 3e6b6a90f3e7484f8a0ca766f8305082 PyFlow.transfer_web.setup_client:3 of +msgid "" +"Starts a lightweight Flask backend on 127.0.0.1 and opens the connect UI " +"in the browser. The user enters the server address (an http/https domain" +" or a bare IP); the backend asks the server's web backend for the TCP " +"server address/port, starts the TCP client, and keeps the backend running" +" to relay the user's frontend actions." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po new file mode 100644 index 0000000..3a28da6 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_server.rst:2 +#: e6642bb55a204188b0abd0b07207713e +msgid "PyFlow.transfer\\_web.setup\\_server module" +msgstr "" + +#: 5966876a4564470ea674311ddb3ef63e PyFlow.transfer_web.setup_server:1 of +msgid "PyFlow TCP server web launcher." +msgstr "" + +#: 588fea0dc68a4a308f9a7cfb6fe5d819 PyFlow.transfer_web.setup_server:3 of +msgid "Checks ``transfer_web/.Flow_Web/setup_server.json``:" +msgstr "" + +#: PyFlow.transfer_web.setup_server:5 c3df4df524da40b18a58607efd9b5e4a of +msgid "" +"missing -> opens the server startup-configuration UI in the browser; the" +" UI saves the config (same shape as ``flow_setup``'s ``setup.json``) and " +"starts the TCP server class;" +msgstr "" + +#: 211850fce6d34d50ae152bce6d1aa3af PyFlow.transfer_web.setup_server:8 of +msgid "present -> starts the TCP server class directly from the saved config." +msgstr "" + +#: PyFlow.transfer_web.setup_server:10 cb510f0a77474eff8e8816fafc097343 of +msgid "" +"After the TCP server is up, the lightweight Flask backend serves the " +"status page and the client-facing API (``/api/server_info`` etc.) on the " +"server's address." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po new file mode 100644 index 0000000..faa4094 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:2 +#: 76090548cb6a4066a3558553adad502a +msgid "PyFlow.transfer\\_web.web\\_backend package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:10 +#: 0d60b135791b45019583e72be4a02afc +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po new file mode 100644 index 0000000..46c52b3 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po @@ -0,0 +1,119 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.server_backend.rst:2 +#: 4e16e69f9ae9491a95bfcab9c70cfe24 +msgid "PyFlow.transfer\\_web.web\\_backend.server\\_backend module" +msgstr "" + +#: 15eb6d4006b3429a8fa230a2b460b1c5 +#: PyFlow.transfer_web.web_backend.server_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP server for the web tool." +msgstr "" + +#: 3c413c9e37f94c7485dba6d4f96b9bf2 +#: PyFlow.transfer_web.web_backend.server_backend:3 of +msgid "Two modes, one process:" +msgstr "" + +#: 557b793ac8e441db86cf59cad5e85371 +#: PyFlow.transfer_web.web_backend.server_backend:5 of +msgid "" +"``config`` mode: serves the server startup-configuration UI. The UI " +"shows every ``TCP_Server_Base`` parameter with its default value; on " +"submit the config is written to ``.Flow_Web/setup_server.json`` (same " +"shape as ``flow_setup``'s ``setup.json``) and the TCP server class is " +"started." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:10 +#: cb922669795b42c6ad5587b527c4356a of +msgid "" +"``status`` mode: serves the minimal status page plus the same " +"sidebar/input UI as the client frontend (forwarding disabled; native " +"sends to connected clients allowed). Also exposes the HTTP API that " +"clients use to discover the TCP server address/port." +msgstr "" + +#: 0c6784517a194d65926c71b7ce7f4836 +#: PyFlow.transfer_web.web_backend.server_backend:15 of +msgid "" +"The backend monitors ``server.clients``: whenever a client connects or " +"disconnects it broadcasts the current instance list to every connected " +"client (``/web_clients_update``), and it re-checks the list every minute." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:20 +#: e902a63f173f4e8ba20923c4d8f4b083 of +msgid "" +"Inbound events (plain-text messages and file uploads arriving from " +"clients) are captured on the TCP server's receive threads through " +"``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` " +"APIs, queued here, and polled by the frontend via ``/api/events``." +msgstr "" + +#: 30582f045fae468cb63a543324edfea8 +#: PyFlow.transfer_web.web_backend.server_backend:25 of +msgid "" +"Authentication: anonymous visitors get a white landing page (the server " +"addresses plus a login button); the configuration and status pages need a" +" session. Accounts live in ``.Flow_Web/users.json``; the first run seeds" +" the ``admin``/``admin`` administrator, and the frontend warns on every " +"login until those default credentials are changed." +msgstr "" + +#: 5fbb587ae4814cd683d5940abf4af37b +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:1 of +msgid "Account store backing the server web login (``.Flow_Web/users.json``)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:3 +#: f16349131c524eba9d2fd07591592afb of +msgid "" +"Passwords are PBKDF2-SHA256 records with a per-user salt. A missing " +"store file seeds the default ``admin``/``admin`` administrator; a store " +"file that exists but cannot be read is *not* re-seeded, so a damaged file" +" can never silently restore the default account." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.authenticate:1 +#: cfd5d429421942b3b0576566b76575a8 of +#, python-brace-format +msgid "Return ``{\"username\", \"role\"}`` for valid credentials, else ``None``." +msgstr "" + +#: 56e3012561ed4ed4aa77ffea3a744f93 +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.change_credentials:1 +#: of +msgid "Rename ``username`` and set its password (self-service)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp:1 +#: ab4c37912c524489a26700248a06704c of +msgid "Flask app + TCP_Server_Base wrapper for the web tool." +msgstr "" + +#: 7e707a76895243cbb7e49d4a943df5f2 +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_server.json`` and start the TCP server." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po new file mode 100644 index 0000000..2b2f939 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.client_backend.rst:2 +#: a7e084a5059341aa9b7419059c4fa4b0 +msgid "PyFlow.transfer\\_web.web\\_front.client\\_backend module" +msgstr "" + +#: 22485674cb53445a86703fb118698523 +#: PyFlow.transfer_web.web_front.client_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP client for the web tool." +msgstr "" + +#: 31862d77d7184e55a1e729d7e72baed0 +#: PyFlow.transfer_web.web_front.client_backend:3 of +msgid "" +"The launcher (``setup_client.py``) starts this backend and opens the " +"connect UI in the browser. The user enters the server address (an " +"``http``/``https`` domain or a bare IP); the backend queries the server's" +" web backend ``/api/server_info`` for the TCP server address and port, " +"then starts the ``TCP_Client_Base`` instance. The backend stays up to " +"relay the user's frontend actions:" +msgstr "" + +#: 1a028258cda844ee945b9522d51afb9d +#: PyFlow.transfer_web.web_front.client_backend:10 of +msgid "messages/files/folders to the server use the native transfer methods;" +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:11 +#: e160c2a0daf9498cbe2041a8e964ff47 of +msgid "" +"messages to other clients use the native ``/forward_send_msg`` forwarding" +" (a client-only command relayed by the server);" +msgstr "" + +#: 16da18234f0a499893b711e7b555b42a +#: PyFlow.transfer_web.web_front.client_backend:13 of +msgid "" +"files/folders to other clients are forwarded through the built-in " +"``forward_extension_tcp`` extension." +msgstr "" + +#: 79dee862a03f4d0c9bc9403f8d461465 +#: PyFlow.transfer_web.web_front.client_backend:16 of +msgid "" +"The sidebar instance list is kept fresh by the server's " +"``/web_clients_update`` broadcasts; a reload button re-requests the list " +"via ``/web_sync_clients``." +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:20 +#: b952223fdafe4d52b8c34498aef1aeef of +msgid "" +"Inbound events (plain-text messages and files pushed by the server, " +"whether direct sends or client forwards) are captured on the TCP client's" +" receive threads through ``TCP_Client_Base``'s " +"``add_message_listener``/``add_file_listener`` APIs, queued here, and " +"polled by the frontend via ``/api/events``." +msgstr "" + +#: 0913e15917534119b775fb5c545c439d +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp:1 of +msgid "Flask app + TCP_Client_Base wrapper for the web tool." +msgstr "" + +#: 3e9a2f40d40b45869c4750ae3e542502 +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_client.json`` and start the TCP client." +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po new file mode 100644 index 0000000..5cb677f --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.rst:2 +#: 24ab9a394e604e0ea77050304ef77edd +msgid "PyFlow.transfer\\_web.web\\_front package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_front.rst:10 +#: ae412dcb5fd34608b462f44c2a8b9a17 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ja/LC_MESSAGES/api/index.po b/docs/locale/ja/LC_MESSAGES/api/index.po new file mode 100644 index 0000000..45f57b7 --- /dev/null +++ b/docs/locale/ja/LC_MESSAGES/api/index.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ja\n" +"Language-Team: ja \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/index.rst:2 8d57b43b04b14ab8a07c05fb89785684 +msgid "API Reference" +msgstr "" + +#: ../../api/index.rst:4 2e33b7c5195747ebb1a15eb5e3e9c026 +msgid "" +"The pages below are generated from the code by ``sphinx-apidoc`` (see the" +" first line of ``docs/reBuild.sh``): each one pulls its text from the " +"docstrings at build time, so nothing here is written by hand." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/File_Transfer/File_Transfer.po b/docs/locale/ko/LC_MESSAGES/File_Transfer/File_Transfer.po index dc9f4de..a2e6af5 100644 --- a/docs/locale/ko/LC_MESSAGES/File_Transfer/File_Transfer.po +++ b/docs/locale/ko/LC_MESSAGES/File_Transfer/File_Transfer.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 13:19+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: ko \n" @@ -693,11 +693,17 @@ msgid "Commands (client console only; rejected on the server console):" msgstr "클라이언트 콘솔(수신자는 서버임):" #: ../../File_Transfer/File_Transfer.rst:413 14406316555743359a854540ab6937c8 -msgid "``/forward_file ... ...``" +#, fuzzy +msgid "" +"``/forward_file ... ... " +"[destination_file_path]``" msgstr "``/forward_file <파일1> <파일2> ... ...``" #: ../../File_Transfer/File_Transfer.rst:414 4dc576898177404da124649f7ce9ac28 -msgid "``/forward_folder ... ...``" +#, fuzzy +msgid "" +"``/forward_folder ... ... " +"[destination_file_path]``" msgstr "``/forward_folder <폴더1> <폴더2> ... ...``" #: ../../File_Transfer/File_Transfer.rst:416 220dfbf0a0d84559a8befb4eee500ba0 @@ -710,11 +716,25 @@ msgstr "" "파일/폴더 수와 대상 클라이언트 수(따옴표로 묶인 주소 튜플로 작성됨)는 무제한입니다. 연결할 수 없거나(서버에 연결되지 않음) 서버 " "자체와 동일한 대상 주소는 건너뛰고 나머지 대상은 계속 제공됩니다." -#: ../../File_Transfer/File_Transfer.rst:424 f6697695e9ef4e3694fa9046e41578e0 +#: ../../File_Transfer/File_Transfer.rst:424 6137bf9406784c6f82d9f41560a52566 +msgid "" +"Like every transfer family, both commands accept an optional trailing " +"``destination_file_path`` that replaces the default save directory on every " +"receiving client: a forwarded file lands at ``/`` and" +" a forwarded folder keeps its structure under " +"``//...``. When the argument is omitted the " +"targets write to their default ``file_transfer_dir``." +msgstr "" +"모든 전송 패밀리와 마찬가지로 두 명령은 모든 수신 클라이언트의 기본 저장 디렉토리를 대체하는 선택적 후행 `` " +"destination_file_path '' 를 허용합니다. 전달된 파일은 ``/`` 에 있고 " +"전달된 폴더는 ''//... `` 아래에 구조를 유지합니다. 인수를 생략하면 대상은 기본 " +"`` file_transfer_dir `` 에 쓸 수 있습니다." + +#: ../../File_Transfer/File_Transfer.rst:435 f6697695e9ef4e3694fa9046e41578e0 msgid "The data path reuses the protocol's own transfer machinery:" msgstr "데이터 경로는 프로토콜의 자체 전송 메커니즘을 재사용합니다." -#: ../../File_Transfer/File_Transfer.rst:427 6ee037f0e4024d2988fcb2ccf3deef70 +#: ../../File_Transfer/File_Transfer.rst:438 6ee037f0e4024d2988fcb2ccf3deef70 msgid "" "The forwarding client streams the file with the standard file-transfer byte " "stream (metadata header + 64 KiB chunks) to a transfer socket on the server." @@ -722,7 +742,7 @@ msgstr "" "전달 클라이언트는 표준 파일 전송 바이트 스트림(메타데이터 헤더 + 64KiB 청크)을 사용하여 파일을 서버의 전송 소켓으로 " "스트리밍합니다." -#: ../../File_Transfer/File_Transfer.rst:432 43c94e746a5c46748846d0dc18a4e0ff +#: ../../File_Transfer/File_Transfer.rst:443 43c94e746a5c46748846d0dc18a4e0ff msgid "" "The server acts as a pure relay: it reads the stream into per-target memory " "queues and writes each chunk to every target's transfer socket. The server " @@ -732,7 +752,7 @@ msgstr "" "서버는 순수 릴레이 역할을 합니다. 즉, 스트림을 대상별 메모리 대기열로 읽고 각 청크를 모든 대상의 전송 소켓에 씁니다. 서버는 크기 " "헤더를 초과하는 파일 내용을 구문 분석하지 않으며 디스크에 쓰지 않습니다." -#: ../../File_Transfer/File_Transfer.rst:439 9dec57916af34e109bee7ce1e0bae2d4 +#: ../../File_Transfer/File_Transfer.rst:450 9dec57916af34e109bee7ce1e0bae2d4 msgid "" "Every target client receives the stream with the ordinary receive path " "(``file_transfer_mode_recv``) and writes it to its own local disk, exactly " @@ -741,11 +761,11 @@ msgstr "" "모든 대상 클라이언트는 일반 수신 경로(``file_transfer_mode_recv``)로 스트림을 수신하고 서버가 파일을 직접 푸시한" " 것처럼 이를 자체 로컬 디스크에 씁니다." -#: ../../File_Transfer/File_Transfer.rst:445 a095abaf821e41a6a66adbb776bc35ef +#: ../../File_Transfer/File_Transfer.rst:456 a095abaf821e41a6a66adbb776bc35ef msgid "### Memory Bounding and Flow Control" msgstr "### 메모리 경계 및 흐름 제어" -#: ../../File_Transfer/File_Transfer.rst:447 244214dd675345f8a72b1088ad99fea0 +#: ../../File_Transfer/File_Transfer.rst:458 244214dd675345f8a72b1088ad99fea0 msgid "" "Because uploader, server and targets may have different bandwidths, data can" " pile up in the server's memory. Both ``TCP_Server_Base`` and " @@ -768,25 +788,25 @@ msgstr "" "배출하므로 버퍼링된 메모리는 단일 청크로 제한됩니다. ``/pause_trans``/``/start_trans`` 핸들러는 양쪽에 " "존재하므로 어느 쪽이든 데이터를 버퍼링할 때 전송을 조절할 수 있습니다." -#: ../../File_Transfer/File_Transfer.rst:472 0aa14deaf6474437b421a65e21fce7d8 +#: ../../File_Transfer/File_Transfer.rst:483 0aa14deaf6474437b421a65e21fce7d8 msgid "Concurrency and Threading" msgstr "동시성과 스레딩" -#: ../../File_Transfer/File_Transfer.rst:474 825c5e1a8b9b405c967ca49a1861e258 +#: ../../File_Transfer/File_Transfer.rst:485 825c5e1a8b9b405c967ca49a1861e258 msgid "" "Both the server and the client use multiple levels of concurrency control to" " ensure stability during file transfers." msgstr "서버와 클라이언트 모두 여러 수준의 동시성 제어를 사용하여 파일 전송 중 안정성을 보장합니다." -#: ../../File_Transfer/File_Transfer.rst:478 0d48b1245cd54597928cfc28d5b3c248 +#: ../../File_Transfer/File_Transfer.rst:489 0d48b1245cd54597928cfc28d5b3c248 msgid "### File Transfer Semaphore" msgstr "### 파일 전송 세마포어" -#: ../../File_Transfer/File_Transfer.rst:480 e2674ee9e0584513bc53aa815603fd24 +#: ../../File_Transfer/File_Transfer.rst:491 e2674ee9e0584513bc53aa815603fd24 msgid "Client: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" msgstr "클라이언트: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:482 1c0d27d13ad844159d29c6e662d51d09 +#: ../../File_Transfer/File_Transfer.rst:493 1c0d27d13ad844159d29c6e662d51d09 msgid "" "Server: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" @@ -794,7 +814,7 @@ msgstr "" "서버: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:485 5b0c8289b2c9460cb7305101226b66a7 +#: ../../File_Transfer/File_Transfer.rst:496 5b0c8289b2c9460cb7305101226b66a7 msgid "" "This semaphore limits the number of simultaneous file transfers (used " "primarily when sending folders or multiple files). Each transfer runs in its" @@ -803,11 +823,11 @@ msgstr "" "이 세마포어는 동시 파일 전송 수를 제한합니다(폴더 또는 여러 파일을 보낼 때 주로 사용됨). 각 전송은 자체 스레드에서 실행되며 " "스레드가 시작되기 전에 세마포어를 획득합니다." -#: ../../File_Transfer/File_Transfer.rst:492 6d93783b298e4ce98b999ff890d4a5f4 +#: ../../File_Transfer/File_Transfer.rst:503 6d93783b298e4ce98b999ff890d4a5f4 msgid "### Threading Model" msgstr "### 스레딩 모델" -#: ../../File_Transfer/File_Transfer.rst:494 036a972600e645119d8a523fa08a52db +#: ../../File_Transfer/File_Transfer.rst:505 036a972600e645119d8a523fa08a52db msgid "" "Each file transfer runs in a dedicated daemon thread, created by the " "``_thread`` wrapper functions (e.g., " @@ -818,7 +838,7 @@ msgstr "" "``file_transfer_client_recv_client_start_thread``)에 의해 생성된 전용 데몬 스레드에서 " "실행됩니다. 이는 느린 전송으로 인해 주 제어 루프가 차단되는 것을 방지합니다." -#: ../../File_Transfer/File_Transfer.rst:500 a84aaf5f9fb64d92b97c32185a1e7cb5 +#: ../../File_Transfer/File_Transfer.rst:511 a84aaf5f9fb64d92b97c32185a1e7cb5 msgid "" "The thread that receives the transfer command (e.g., the server's " "``handle_command`` thread) does not wait for the transfer to complete; it " @@ -827,7 +847,7 @@ msgstr "" "전송 명령을 받는 스레드(예: 서버의 ``handle_command`` 스레드)는 전송이 완료될 때까지 기다리지 않습니다. 작업자 " "스레드를 생성한 후 즉시 반환됩니다." -#: ../../File_Transfer/File_Transfer.rst:505 669fb6da8e7444dd829a6e14295648b1 +#: ../../File_Transfer/File_Transfer.rst:516 669fb6da8e7444dd829a6e14295648b1 msgid "" "The low-level receive function (``file_transfer_mode_recv``) blocks while " "reading from the transfer socket, but because it runs in a dedicated thread," @@ -836,11 +856,11 @@ msgstr "" "낮은 수준의 수신 기능(``file_transfer_mode_recv``)은 전송 소켓에서 읽는 동안 차단되지만 전용 스레드에서 실행되기" " 때문에 기본 연결은 계속 응답합니다." -#: ../../File_Transfer/File_Transfer.rst:511 e39f8ae06ae84a6b84de8a3008ca17cb +#: ../../File_Transfer/File_Transfer.rst:522 e39f8ae06ae84a6b84de8a3008ca17cb msgid "### Thread Pool for Custom Commands" msgstr "### 사용자 정의 명령을 위한 스레드 풀" -#: ../../File_Transfer/File_Transfer.rst:513 3ad49853529d4563bacdfb4ca6eee115 +#: ../../File_Transfer/File_Transfer.rst:524 3ad49853529d4563bacdfb4ca6eee115 msgid "" "Both classes also provide a ``ThreadPoolExecutor`` " "(``self._custom_executor``) for custom command handlers. When a handler is " @@ -855,11 +875,11 @@ msgstr "" "``max_custom_workers``로 제한하기 위해 세마포어도 사용합니다. 이 메커니즘은 파일 전송 세마포어와 **독립적**이며 " "범용 명령 처리를 위한 것입니다." -#: ../../File_Transfer/File_Transfer.rst:528 1862d74da28a40219f82cfd7af89c126 +#: ../../File_Transfer/File_Transfer.rst:539 1862d74da28a40219f82cfd7af89c126 msgid "Port Allocation and Management" msgstr "포트 할당 및 관리" -#: ../../File_Transfer/File_Transfer.rst:530 bd53ec8aa4ef498aa0815db16ed03140 +#: ../../File_Transfer/File_Transfer.rst:541 bd53ec8aa4ef498aa0815db16ed03140 msgid "" "File transfers require ephemeral ports for the secondary data connections. " "The ``palloc()`` and ``pfree()`` methods are used to obtain and release " @@ -868,7 +888,7 @@ msgstr "" "파일 전송에는 보조 데이터 연결을 위한 임시 포트가 필요합니다. ``palloc()`` 및 ``pfree()`` 메소드는 이러한 포트를 " "획득하고 해제하는 데 사용됩니다. 두 가지 모드를 사용할 수 있습니다:" -#: ../../File_Transfer/File_Transfer.rst:536 eeaa9379d7d941159ac34f30ff6bb5f9 +#: ../../File_Transfer/File_Transfer.rst:547 eeaa9379d7d941159ac34f30ff6bb5f9 msgid "" "**Automatic mode** (``is_hand_alloc_port=False``): ``palloc()`` returns " "``0``, and the operating system assigns a free port when the socket is " @@ -877,7 +897,7 @@ msgstr "" "**자동 모드** (``is_hand_alloc_port=False``): ``palloc()``은 ``0``을 반환하고, 소켓이 " "바인딩될 때 운영 체제는 사용 가능한 포트를 할당합니다. 이는 대부분의 사용 사례에 권장되는 모드입니다." -#: ../../File_Transfer/File_Transfer.rst:541 80e08ac6614b4dcd88cd0e4d31a3da64 +#: ../../File_Transfer/File_Transfer.rst:552 80e08ac6614b4dcd88cd0e4d31a3da64 msgid "" "**Manual mode** (``is_hand_alloc_port=True``): Ports are drawn from a " "configurable range ``[self.min_port, self.max_port]`` with a step size " @@ -890,7 +910,7 @@ msgstr "" "``/client_alloc_port_range``를 통해 허용 범위를 클라이언트에 브로드캐스트하고 클라이언트는 동일한 수동 할당 논리를" " 사용합니다." -#: ../../File_Transfer/File_Transfer.rst:551 29cfdab183eb4f9f83de41ef62a60785 +#: ../../File_Transfer/File_Transfer.rst:562 29cfdab183eb4f9f83de41ef62a60785 msgid "" "*Note: For more details about port allocation, please visit the Port " "Allocation API sections in :doc:`TCP_Server_APIs` and " @@ -899,15 +919,15 @@ msgstr "" "*참고: 포트 할당에 대한 자세한 내용은 :doc:`TCP_Server_APIs` 및 :doc:`TCP_Client_APIs`의 포트 " "할당 API 섹션을 참조하세요.*" -#: ../../File_Transfer/File_Transfer.rst:559 189736d8d1f8464b8d301f219700fda4 +#: ../../File_Transfer/File_Transfer.rst:570 189736d8d1f8464b8d301f219700fda4 msgid "Error Handling and Timeouts" msgstr "오류 처리 및 시간 초과" -#: ../../File_Transfer/File_Transfer.rst:561 bcc373777989436f9df3657446d1a7f2 +#: ../../File_Transfer/File_Transfer.rst:572 bcc373777989436f9df3657446d1a7f2 msgid "### Timeout Values" msgstr "### 시간 초과 값" -#: ../../File_Transfer/File_Transfer.rst:563 27d75d097a804ec28ebec5c67469c9c6 +#: ../../File_Transfer/File_Transfer.rst:574 27d75d097a804ec28ebec5c67469c9c6 msgid "" "**Start signal timeout**: 10 seconds. If the receiver does not send " "``server_start_file_transfer_sign`` within this time, the sender aborts." @@ -915,14 +935,14 @@ msgstr "" "**시작 신호 시간 초과**: 10초. 수신자가 이 시간 내에 ``server_start_file_transfer_sign``을 보내지 " "않으면 발신자가 중단됩니다." -#: ../../File_Transfer/File_Transfer.rst:567 93e877c411eb4c96bd7861ff61f35e7a +#: ../../File_Transfer/File_Transfer.rst:578 93e877c411eb4c96bd7861ff61f35e7a msgid "" "**Port negotiation timeout**: 20 seconds. The initiator waits for the peer's" " ``/server_file_transfer_port`` response." msgstr "" "**포트 협상 시간 초과**: 20초. 개시자는 피어의 ``/server_file_transfer_port`` 응답을 기다립니다." -#: ../../File_Transfer/File_Transfer.rst:570 9e1d395bb59540a49393bebe6630e5eb +#: ../../File_Transfer/File_Transfer.rst:581 9e1d395bb59540a49393bebe6630e5eb msgid "" "**Completion acknowledgement timeout**: ``30 + (file_size // (100 * 1024 * " "1024)) * 10`` seconds. Larger files get proportionally more time." @@ -930,33 +950,33 @@ msgstr "" "**완료 확인 시간 초과**: ``30 + (file_size // (100 * 1024 * 1024)) * 10``초. 파일이 클수록 " "비례적으로 더 많은 시간이 소요됩니다." -#: ../../File_Transfer/File_Transfer.rst:574 6b4e6135669b41178fdbb47ea5975381 +#: ../../File_Transfer/File_Transfer.rst:585 6b4e6135669b41178fdbb47ea5975381 msgid "### Error Signalling" msgstr "### 오류 신호" -#: ../../File_Transfer/File_Transfer.rst:576 448fdc6ba3dd4d9eae925e7e241f8cb8 +#: ../../File_Transfer/File_Transfer.rst:587 448fdc6ba3dd4d9eae925e7e241f8cb8 msgid "" "Any error during the handshake or data transfer causes the failing side to " "send ``error_sign`` over the transfer socket." msgstr "핸드셰이크나 데이터 전송 중 오류가 발생하면 실패한 쪽에서 전송 소켓을 통해 ``error_sign``을 보냅니다." -#: ../../File_Transfer/File_Transfer.rst:579 5b4c23a93e984ce0af2e43cf4752225b +#: ../../File_Transfer/File_Transfer.rst:590 5b4c23a93e984ce0af2e43cf4752225b msgid "" "The other side, upon receiving the error sign, closes the transfer socket " "and aborts the transfer." msgstr "상대방은 오류 신호를 받으면 전송 소켓을 닫고 전송을 중단합니다." -#: ../../File_Transfer/File_Transfer.rst:582 b337a2726ded4d619c5e8026bef3f6ea +#: ../../File_Transfer/File_Transfer.rst:593 b337a2726ded4d619c5e8026bef3f6ea msgid "" "The main control connection remains unaffected; only the transfer socket is " "closed." msgstr "메인 제어 연결은 영향을 받지 않습니다. 전송 소켓만 닫혀 있습니다." -#: ../../File_Transfer/File_Transfer.rst:586 68be52c8e5ce447a9c5ec51a661229cf +#: ../../File_Transfer/File_Transfer.rst:597 68be52c8e5ce447a9c5ec51a661229cf msgid "### Exception Handling" msgstr "### 예외 처리" -#: ../../File_Transfer/File_Transfer.rst:588 036db9bc1858417ca589816028b83f80 +#: ../../File_Transfer/File_Transfer.rst:599 036db9bc1858417ca589816028b83f80 msgid "" "All socket operations are wrapped in try-except blocks. When an exception " "occurs (e.g., connection reset, file not found), the error is logged with " @@ -967,11 +987,11 @@ msgstr "" "``traceback.print_exc()``를 사용하여 오류가 기록되고 전송이 정상적으로 중단됩니다. 가능한 경우 " "``error_sign``을 전송하고 전송 소켓을 닫습니다." -#: ../../File_Transfer/File_Transfer.rst:600 fe9377db9cf04b9da0dbe4d07c730adf +#: ../../File_Transfer/File_Transfer.rst:611 fe9377db9cf04b9da0dbe4d07c730adf msgid "Related API Definitions" msgstr "관련 API 정의" -#: ../../File_Transfer/File_Transfer.rst:602 cc71cb9b1d6642a2acc89d45a49022cc +#: ../../File_Transfer/File_Transfer.rst:613 cc71cb9b1d6642a2acc89d45a49022cc msgid "" "This section lists all public file-transfer related methods in " "``TCP_Server_Base`` and ``TCP_Client_Base``. For a complete list of all " @@ -980,11 +1000,11 @@ msgstr "" "이 섹션에는 ``TCP_Server_Base`` 및 ``TCP_Client_Base``의 모든 공용 파일 전송 관련 방법이 나열되어 " "있습니다. 모든 공개 API의 전체 목록을 보려면 이 문서 끝에 있는 표를 참조하세요." -#: ../../File_Transfer/File_Transfer.rst:608 d6683e415f794c5bb693f8c24370e7f9 +#: ../../File_Transfer/File_Transfer.rst:619 d6683e415f794c5bb693f8c24370e7f9 msgid "### Server-Side File Transfer APIs" msgstr "### 서버측 파일 전송 API" -#: ../../File_Transfer/File_Transfer.rst:618 9123c69197c34d93bb68d088897ebeca +#: ../../File_Transfer/File_Transfer.rst:629 9123c69197c34d93bb68d088897ebeca msgid "" "Initiates a server-to-client file transfer. ``message`` is the command " "string (e.g., ``/file /path/to/file.txt (127.0.0.1,54321)``). If " @@ -995,11 +1015,11 @@ msgstr "" "(127.0.0.1,54321)``). ``file_folder_abspath``가 제공되면(폴더 전송의 경우) 상위 폴더의 절대 경로를" " 지정합니다." -#: ../../File_Transfer/File_Transfer.rst:633 367dfd82c95c413d963a15152469fc44 +#: ../../File_Transfer/File_Transfer.rst:644 367dfd82c95c413d963a15152469fc44 msgid "Thread-safe version that starts a new thread for the transfer." msgstr "전송을 위해 새 스레드를 시작하는 스레드 안전 버전입니다." -#: ../../File_Transfer/File_Transfer.rst:642 d4aa76b7eb2c46c29adee0120a939b66 +#: ../../File_Transfer/File_Transfer.rst:653 d4aa76b7eb2c46c29adee0120a939b66 msgid "" "Sends an entire folder from server to client. ``message`` should be of the " "form ``/file_folder ``." @@ -1007,7 +1027,7 @@ msgstr "" "서버에서 클라이언트로 전체 폴더를 보냅니다. ``메시지``는 ``/file_folder " "`` 형식이어야 합니다." -#: ../../File_Transfer/File_Transfer.rst:652 fcb785ff144746fab81e95ec2ab056e1 +#: ../../File_Transfer/File_Transfer.rst:663 fcb785ff144746fab81e95ec2ab056e1 msgid "" "Sends multiple files to multiple clients. The message format is " "``/multiple_file_multiple_client ... " @@ -1016,7 +1036,7 @@ msgstr "" "여러 클라이언트에 여러 파일을 보냅니다. 메시지 형식은 ``/multiple_file_multiple_client " " ... ...``입니다. 파일은 클라이언트 앞에 나타나야 합니다." -#: ../../File_Transfer/File_Transfer.rst:664 9c6f024528f344e399b62023c7f8c858 +#: ../../File_Transfer/File_Transfer.rst:675 9c6f024528f344e399b62023c7f8c858 msgid "" "Sends different file lists to different clients. The message alternates " "between groups: a list of files, then a list of client addresses, then the " @@ -1027,29 +1047,29 @@ msgstr "" "표시됩니다. 예: ``/diff_multiple_file_diff_multiple_client a.txt b.txt (ip1,port1)" " (ip2,port2) c.txt (ip3,port3)``" -#: ../../File_Transfer/File_Transfer.rst:682 f8a880287b7b49d4bdc2239ecf4a0577 +#: ../../File_Transfer/File_Transfer.rst:693 f8a880287b7b49d4bdc2239ecf4a0577 msgid "" "Receives a file from a client. Called internally when the server receives a " "``/file`` command from a client." msgstr "클라이언트로부터 파일을 받습니다. 서버가 클라이언트로부터 ``/file`` 명령을 수신하면 내부적으로 호출됩니다." -#: ../../File_Transfer/File_Transfer.rst:698 97bfaa9c24ae47c39328707b8f17a91a +#: ../../File_Transfer/File_Transfer.rst:709 97bfaa9c24ae47c39328707b8f17a91a msgid "" "Low-level receive function that performs the handshake and writes the " "incoming file to disk." msgstr "핸드셰이크를 수행하고 수신 파일을 디스크에 쓰는 낮은 수준의 수신 기능입니다." -#: ../../File_Transfer/File_Transfer.rst:711 db6a97ca42ba43e59d5c20695039d4ee +#: ../../File_Transfer/File_Transfer.rst:722 db6a97ca42ba43e59d5c20695039d4ee msgid "" "Low-level send function that connects to the receiver and transmits the " "file." msgstr "수신자와 연결하여 파일을 전송하는 저수준 전송 기능입니다." -#: ../../File_Transfer/File_Transfer.rst:713 3e7b689215e840bebd368b4d29104ebc +#: ../../File_Transfer/File_Transfer.rst:724 3e7b689215e840bebd368b4d29104ebc msgid "### Client-Side File Transfer APIs" msgstr "### 클라이언트측 파일 전송 API" -#: ../../File_Transfer/File_Transfer.rst:723 1e97e492d6504579a9a265eb1242395e +#: ../../File_Transfer/File_Transfer.rst:734 1e97e492d6504579a9a265eb1242395e msgid "" "Initiates a client-to-server file transfer. ``message`` is the user command " "(e.g., ``/file mydoc.txt``). Used internally by the interactive console." @@ -1057,79 +1077,79 @@ msgstr "" "클라이언트-서버 파일 전송을 시작합니다. ``message``는 사용자 명령입니다(예: ``/file mydoc.txt``). 대화형 " "콘솔에서 내부적으로 사용됩니다." -#: ../../File_Transfer/File_Transfer.rst:735 -#: ../../File_Transfer/File_Transfer.rst:786 0cd2695763114a0b831df0bfa80a3d56 +#: ../../File_Transfer/File_Transfer.rst:746 +#: ../../File_Transfer/File_Transfer.rst:797 0cd2695763114a0b831df0bfa80a3d56 msgid "Thread-safe version." msgstr "스레드로부터 안전한 버전." -#: ../../File_Transfer/File_Transfer.rst:744 261399ca508d463eafa7f03a00bfc658 +#: ../../File_Transfer/File_Transfer.rst:755 261399ca508d463eafa7f03a00bfc658 msgid "Sends a folder from client to server." msgstr "클라이언트에서 서버로 폴더를 보냅니다." -#: ../../File_Transfer/File_Transfer.rst:753 ef5e3b92b11c4530960c1c344a51c73b +#: ../../File_Transfer/File_Transfer.rst:764 ef5e3b92b11c4530960c1c344a51c73b msgid "Sends multiple files from client to server." msgstr "클라이언트에서 서버로 여러 파일을 보냅니다." -#: ../../File_Transfer/File_Transfer.rst:762 f2c90ee949d7484480cbb2cd5310bf26 +#: ../../File_Transfer/File_Transfer.rst:773 f2c90ee949d7484480cbb2cd5310bf26 msgid "Sends multiple folders from client to server." msgstr "클라이언트에서 서버로 여러 폴더를 보냅니다." -#: ../../File_Transfer/File_Transfer.rst:775 0b13d26a40174243a15698b4bfcbb69f +#: ../../File_Transfer/File_Transfer.rst:786 0b13d26a40174243a15698b4bfcbb69f msgid "" "Receives a file from the server (called when the server initiates a " "transfer)." msgstr "서버로부터 파일을 수신합니다(서버가 전송을 시작할 때 호출됨)." -#: ../../File_Transfer/File_Transfer.rst:797 3b78a4355d7f4ee2bbbe6bf934a962c0 +#: ../../File_Transfer/File_Transfer.rst:808 3b78a4355d7f4ee2bbbe6bf934a962c0 msgid "Receives a folder from the server." msgstr "서버로부터 폴더를 받습니다." -#: ../../File_Transfer/File_Transfer.rst:812 3d044e0754b94d1289b491502ce83610 +#: ../../File_Transfer/File_Transfer.rst:823 3d044e0754b94d1289b491502ce83610 msgid "Low-level receive function on the client side." msgstr "클라이언트 측의 낮은 수준 수신 기능." -#: ../../File_Transfer/File_Transfer.rst:824 7311023a7fa644ed9b57a2873cd3bca8 +#: ../../File_Transfer/File_Transfer.rst:835 7311023a7fa644ed9b57a2873cd3bca8 msgid "" "Low‑level send function on the client side (identical to server's version)." msgstr "클라이언트 측의 낮은 수준 전송 기능(서버 버전과 동일)" -#: ../../File_Transfer/File_Transfer.rst:829 7e74e9a09a8a4cf2a8a372a50b1ee51b +#: ../../File_Transfer/File_Transfer.rst:840 7e74e9a09a8a4cf2a8a372a50b1ee51b msgid "Public API Summary" msgstr "공개 API 요약" -#: ../../File_Transfer/File_Transfer.rst:831 87aaf0a5d3b44f41a419d97b6567f6d0 +#: ../../File_Transfer/File_Transfer.rst:842 87aaf0a5d3b44f41a419d97b6567f6d0 msgid "" "All public APIs (including non-file-transfer methods) are listed below for " "reference." msgstr "모든 공개 API(비파일 전송 방법 포함)는 참조용으로 아래에 나열되어 있습니다." -#: ../../File_Transfer/File_Transfer.rst:835 a195ee393f2a442c810e59811a6ae126 +#: ../../File_Transfer/File_Transfer.rst:846 a195ee393f2a442c810e59811a6ae126 msgid "### TCP_Server_Base Public APIs" msgstr "### TCP_Server_Base 공개 API" -#: ../../File_Transfer/File_Transfer.rst:837 0408d74a9140472e9a774143a60e5749 +#: ../../File_Transfer/File_Transfer.rst:848 0408d74a9140472e9a774143a60e5749 msgid "``file_transfer_server_recv_client_start``" msgstr "``file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:838 24a33cb212844e04a8a271ada32412f0 +#: ../../File_Transfer/File_Transfer.rst:849 24a33cb212844e04a8a271ada32412f0 msgid "``file_transfer_server_recv_client_start_thread``" msgstr "``file_transfer_server_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:839 7e286d1ece6940868bea6b937486e617 +#: ../../File_Transfer/File_Transfer.rst:850 7e286d1ece6940868bea6b937486e617 msgid "``folder_file_transfer_server_recv_client_start``" msgstr "``folder_file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:840 37fc5bd346c84415bb15138a42508fbe +#: ../../File_Transfer/File_Transfer.rst:851 37fc5bd346c84415bb15138a42508fbe msgid "``multiple_file_multiple_client_transfer_server_recv_client_start``" msgstr "``multiple_file_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:841 38a2b16e3ab648688cf7e8d1cae8be72 +#: ../../File_Transfer/File_Transfer.rst:852 38a2b16e3ab648688cf7e8d1cae8be72 msgid "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" msgstr "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:843 11476e0eaa3b4a4286612812e4e2c004 +#: ../../File_Transfer/File_Transfer.rst:854 11476e0eaa3b4a4286612812e4e2c004 msgid "" "(The low-level helpers ``file_transfer_server_recv_server_start``, " "``file_transfer_mode_recv``, and ``file_transfer_mode`` are not considered " @@ -1139,65 +1159,65 @@ msgstr "" "``file_transfer_mode_recv`` 및 ``file_transfer_mode``는 공개로 간주되지 않지만 완전성을 위해 " "문서화되어 있습니다.)" -#: ../../File_Transfer/File_Transfer.rst:849 907e861cebe648fbacb799bae8bb15e0 +#: ../../File_Transfer/File_Transfer.rst:860 907e861cebe648fbacb799bae8bb15e0 msgid "### TCP_Client_Base Public APIs" msgstr "### TCP_Client_Base 공개 API" -#: ../../File_Transfer/File_Transfer.rst:851 88bd80083f754caeb026f9ce1b8c6b55 +#: ../../File_Transfer/File_Transfer.rst:862 88bd80083f754caeb026f9ce1b8c6b55 msgid "``file_transfer_client_recv_client_start``" msgstr "``file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:852 3b21ff73da694465906482412b4fb4e3 +#: ../../File_Transfer/File_Transfer.rst:863 3b21ff73da694465906482412b4fb4e3 msgid "``file_transfer_client_recv_client_start_thread``" msgstr "``file_transfer_client_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:853 c8af43896a2443a5b14bd29863731c2c +#: ../../File_Transfer/File_Transfer.rst:864 c8af43896a2443a5b14bd29863731c2c msgid "``folder_file_transfer_client_recv_client_start``" msgstr "``folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:854 7a3d3e4a52334b169a62d3b30d7a3190 +#: ../../File_Transfer/File_Transfer.rst:865 7a3d3e4a52334b169a62d3b30d7a3190 msgid "``multiple_file_transfer_client_recv_client_start``" msgstr "``multiple_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:855 19e18754c4674842ad5116f709588e03 +#: ../../File_Transfer/File_Transfer.rst:866 19e18754c4674842ad5116f709588e03 msgid "``multiple_folder_file_transfer_client_recv_client_start``" msgstr "``multiple_folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:856 4cf80f9f34e045c59a680d0450c9103a +#: ../../File_Transfer/File_Transfer.rst:867 4cf80f9f34e045c59a680d0450c9103a msgid "``file_transfer_client_recv_server_start``" msgstr "``file_transfer_client_recv_server_start``" -#: ../../File_Transfer/File_Transfer.rst:857 c09f5fcba03f461f89238dd31abf6e88 +#: ../../File_Transfer/File_Transfer.rst:868 c09f5fcba03f461f89238dd31abf6e88 msgid "``file_transfer_client_recv_server_start_thread``" msgstr "``file_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:858 ad39bc6b71f048b09bca3b895e9d32f8 +#: ../../File_Transfer/File_Transfer.rst:869 ad39bc6b71f048b09bca3b895e9d32f8 msgid "``file_folder_transfer_client_recv_server_start_thread``" msgstr "``file_folder_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:860 99140d0be72649199911a57a26e2f2cf +#: ../../File_Transfer/File_Transfer.rst:871 99140d0be72649199911a57a26e2f2cf msgid "(The low-level helpers are documented but not part of the public API.)" msgstr "(저수준 도우미는 문서화되어 있지만 공개 API의 일부는 아닙니다.)" -#: ../../File_Transfer/File_Transfer.rst:864 60355543ac904504af8529431ce2c1fa +#: ../../File_Transfer/File_Transfer.rst:875 60355543ac904504af8529431ce2c1fa msgid "See Also" msgstr "참조" -#: ../../File_Transfer/File_Transfer.rst:866 7a5902ad3de64bf79d54d7f2f83ecbfb +#: ../../File_Transfer/File_Transfer.rst:877 7a5902ad3de64bf79d54d7f2f83ecbfb msgid "" "For more information about the TCP server and client base classes, please " "refer to:" msgstr "TCP 서버 및 클라이언트 기본 클래스에 대한 자세한 내용은 다음을 참조하세요." -#: ../../File_Transfer/File_Transfer.rst:870 f10c29f467b849e8b4254998b44f99ba +#: ../../File_Transfer/File_Transfer.rst:881 f10c29f467b849e8b4254998b44f99ba msgid ":doc:`../Network_APIs/TCP_Server_APIs`" msgstr ":doc:`../Network_APIs/TCP_Server_APIs`" -#: ../../File_Transfer/File_Transfer.rst:871 335ba244d28342449db065c252d7e14c +#: ../../File_Transfer/File_Transfer.rst:882 335ba244d28342449db065c252d7e14c msgid ":doc:`../Network_APIs/TCP_Client_APIs`" msgstr ":doc:`../Network_APIs/TCP_Client_APIs`" -#: ../../File_Transfer/File_Transfer.rst:873 92d0227c356447a098cba072d5b43c98 +#: ../../File_Transfer/File_Transfer.rst:884 92d0227c356447a098cba072d5b43c98 msgid "" "For details on port allocation, see the Port Allocation API sections in " "those documents." diff --git a/docs/locale/ko/LC_MESSAGES/Instance_Setup/Instance_Setup.po b/docs/locale/ko/LC_MESSAGES/Instance_Setup/Instance_Setup.po index 3df41bc..35801df 100644 --- a/docs/locale/ko/LC_MESSAGES/Instance_Setup/Instance_Setup.po +++ b/docs/locale/ko/LC_MESSAGES/Instance_Setup/Instance_Setup.po @@ -8,20 +8,20 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-30 23:45+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.18.0\n" -#: ../../Instance_Setup/Instance_Setup.rst:3 707444a8cdb246fd81a189c038518c82 +#: ../../Instance_Setup/Instance_Setup.rst:3 552b0fbec3774a638b0029ce5fd72949 msgid "Flow Setup Launcher" msgstr "흐름 설정 실행기" -#: ../../Instance_Setup/Instance_Setup.rst:5 a611d1bbc1bc4b12a9db0997909626bc +#: ../../Instance_Setup/Instance_Setup.rst:5 31952c3324264ea28368c7725796ff63 msgid "" "The ``flow_setup.py`` script is a launcher for the TCP server/client " "framework defined in ``connect_tcp.py``. It allows you to quickly spawn a " @@ -33,7 +33,7 @@ msgstr "" "프로그램입니다. 이를 통해 대화형으로 또는 명령줄 인수를 통해 단일 서버 또는 클라이언트 인스턴스를 빠르게 생성할 수 있습니다. 시작된 " "각 인스턴스는 별도의 터미널 창(또는 헤드리스 시스템의 백그라운드 프로세스)에서 실행됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:12 bc0bbe4590e04437827678fe85f482fb +#: ../../Instance_Setup/Instance_Setup.rst:12 4f99dd7362fa4dcb8f83de37f327dbef msgid "" "**Note:** This launcher supports only **one server** and **one client** " "instance at a time. Adding a new server or client configuration will " @@ -42,20 +42,20 @@ msgstr "" "**참고:** 이 실행 프로그램은 한 번에 **하나의 서버**와 **하나의 클라이언트** 인스턴스만 지원합니다. 새 서버 또는 클라이언트" " 구성을 추가하면 동일한 유형의 이전 구성을 완전히 덮어쓰게 됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:18 146d87f809054469be52a0d4174907fc +#: ../../Instance_Setup/Instance_Setup.rst:18 c8f9652279de42bb983523a15048596e msgid "Features" msgstr "특징" -#: ../../Instance_Setup/Instance_Setup.rst:20 ea5ca94177b349b3852287e9327f792e +#: ../../Instance_Setup/Instance_Setup.rst:20 fba5b9ea870c44fabf69022a45cdaebc msgid "" "**Interactive mode** – step‑by‑step creation of a server or client instance." msgstr "**대화형 모드** – 서버 또는 클라이언트 인스턴스를 단계별로 생성합니다." -#: ../../Instance_Setup/Instance_Setup.rst:22 bb06f53ddbea4dd09b99e288569f858d +#: ../../Instance_Setup/Instance_Setup.rst:22 df9cbf9e1d024f9386a4b6278c82249e msgid "**Command‑line mode** – launch with all parameters in one command." msgstr "**명령줄 모드** – 하나의 명령으로 모든 매개변수를 사용하여 실행합니다." -#: ../../Instance_Setup/Instance_Setup.rst:24 11bd4b3ce97743e0857c109ae9bc2286 +#: ../../Instance_Setup/Instance_Setup.rst:24 5f7cba6df26144e9840d140f94065b33 msgid "" "**Persistent configuration** – stores the latest instance definitions in " "``setup.json`` (same directory as the script). Each type (server/client) " @@ -64,7 +64,7 @@ msgstr "" "**영구 구성** – 최신 인스턴스 정의를 ``setup.json``(스크립트와 동일한 디렉터리)에 저장합니다. 각 " "유형(서버/클라이언트)에는 **하나의** 구성만 포함되며 업데이트할 때마다 덮어쓰여집니다." -#: ../../Instance_Setup/Instance_Setup.rst:29 7b3f201c93c349719f4128f05cc194bf +#: ../../Instance_Setup/Instance_Setup.rst:29 be9bc008af594306be21e731d6aefee3 msgid "" "**Cross‑platform** – supports Windows (cmd), Linux (gnome‑terminal, xterm, " "or background), and macOS (Terminal.app)." @@ -72,7 +72,7 @@ msgstr "" "**크로스 플랫폼** – Windows(cmd), Linux(gnome 터미널, xterm 또는 백그라운드) 및 " "macOS(Terminal.app)를 지원합니다." -#: ../../Instance_Setup/Instance_Setup.rst:32 89b17f1fa60241a288381f74072aa273 +#: ../../Instance_Setup/Instance_Setup.rst:32 57005cb0d04f47b2a8f10ad0bc9c7b43 msgid "" "**Complete parameter support** – all parameters accepted by " "``TCP_Server_Base`` and ``TCP_Client_Base`` can be stored in ``setup.json`` " @@ -81,41 +81,41 @@ msgstr "" "**완전한 매개변수 지원** – ``TCP_Server_Base`` 및 ``TCP_Client_Base``에서 허용하는 모든 매개변수는 " "미세 조정을 위해 ``setup.json``에 저장할 수 있습니다." -#: ../../Instance_Setup/Instance_Setup.rst:37 ca859ffa1d4140cab68092170525c59f +#: ../../Instance_Setup/Instance_Setup.rst:37 2ff27919a6f64e8fb610158de5ffdf69 msgid "Usage" msgstr "용법" -#: ../../Instance_Setup/Instance_Setup.rst:40 1935db82ef7f4dbeb08d386a49aba876 +#: ../../Instance_Setup/Instance_Setup.rst:40 dddde4f2d7084bc48efa1296cfd3f22c msgid "Interactive Mode" msgstr "대화형 모드" -#: ../../Instance_Setup/Instance_Setup.rst:42 86f8dafb1c9d4adaa4666b1dea52af23 +#: ../../Instance_Setup/Instance_Setup.rst:42 f060e64959674f52b223bbc4b01d568c msgid "Run the script without any arguments:" msgstr "인수 없이 스크립트를 실행합니다." -#: ../../Instance_Setup/Instance_Setup.rst:48 d516fe21071e448da15df91f07353fc8 +#: ../../Instance_Setup/Instance_Setup.rst:48 dd76918a5ffd44838c1b0c44595ae55b msgid "The script will ask you to:" msgstr "스크립트는 다음을 요청합니다." -#: ../../Instance_Setup/Instance_Setup.rst:50 41684f03f6d7460f87fed8f4bef8f1e5 +#: ../../Instance_Setup/Instance_Setup.rst:50 80847dcc3e404a779b2e693cf8de50d5 msgid "Choose the type (0 for Server, 1 for Client)." msgstr "유형을 선택합니다(서버의 경우 0, 클라이언트의 경우 1)." -#: ../../Instance_Setup/Instance_Setup.rst:51 750440862d7545e9bc4f137f36983fd6 +#: ../../Instance_Setup/Instance_Setup.rst:51 cebc8ba62d1a48a69d8019c3862dc18b msgid "Enter the bind address and port (``host:port``)." msgstr "바인딩 주소와 포트(``host:port``)를 입력하세요." -#: ../../Instance_Setup/Instance_Setup.rst:52 a938572ed1dd43f7988e382ec9fea306 +#: ../../Instance_Setup/Instance_Setup.rst:52 4311b66e1b974b4596f2693908ba3109 msgid "If Client, also enter the server address and port to connect to." msgstr "클라이언트인 경우 연결할 서버 주소와 포트도 입력합니다." -#: ../../Instance_Setup/Instance_Setup.rst:53 3f30b7f3c46e42c19eae6344b424e1f7 +#: ../../Instance_Setup/Instance_Setup.rst:53 65c194c1b655417c921c1e03daf7a17c msgid "" "Decide whether to add another instance (if you add the same type again, the " "previous configuration of that type is overwritten)." msgstr "다른 인스턴스를 추가할지 여부를 결정합니다. 동일한 유형을 다시 추가하면 해당 유형의 이전 구성을 덮어씁니다." -#: ../../Instance_Setup/Instance_Setup.rst:55 e8b6550fe5eb426f8478c1b3fef81160 +#: ../../Instance_Setup/Instance_Setup.rst:55 6bb60d44328244d4ab20f3ae5ba2b827 msgid "" "If ``setup.json`` already exists, you will be prompted to either reuse the " "existing configuration (launch the stored instances) or overwrite it with " @@ -124,67 +124,113 @@ msgstr "" "``setup.json``이 이미 존재하는 경우 기존 구성을 재사용하거나(저장된 인스턴스 시작) 이를 새 정의로 덮어쓰라는 메시지가 " "표시됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:60 b222bf87298747c6b526d72e1459ad83 +#: ../../Instance_Setup/Instance_Setup.rst:60 9aeb2efa913341a89212e86cb7419e5a msgid "" "**Important:** When you choose to overwrite, the old server/client " "configuration is **completely replaced** by the new one. There is no " "merging." msgstr "**중요:** 덮어쓰기를 선택하면 이전 서버/클라이언트 구성이 새 구성으로 **완전히 교체**됩니다. 병합이 없습니다." -#: ../../Instance_Setup/Instance_Setup.rst:65 ad560cd1d404495583c74b5929235453 +#: ../../Instance_Setup/Instance_Setup.rst:65 b5cd8be69a40420a89cf7ad69631cd45 msgid "Command‑line Mode" msgstr "명령줄 모드" -#: ../../Instance_Setup/Instance_Setup.rst:67 50a9125f52a54c9b82c66cf616e06d71 +#: ../../Instance_Setup/Instance_Setup.rst:67 d798ec606de2454da5f10e9d0a7e677c msgid "Use the following options:" msgstr "다음 옵션을 사용하세요." -#: ../../Instance_Setup/Instance_Setup.rst:82 f7f3b3263fdf457c908ea18b74dccea9 +#: ../../Instance_Setup/Instance_Setup.rst:70 4d5c1aebbede473c8598cbc45f8deb20 +msgid "Option" +msgstr "옵션" + +#: ../../Instance_Setup/Instance_Setup.rst:70 a50ba126b168482997fae00497252fd4 +msgid "Description" +msgstr "설명" + +#: ../../Instance_Setup/Instance_Setup.rst:72 7755904907ee46f6a34144360ed876f7 +#, python-brace-format +msgid "``--type {0,1}``" +msgstr "`` --type {0,1} ``" + +#: ../../Instance_Setup/Instance_Setup.rst:72 e0533cce8d9f4586aa838073b1ed3e2a +msgid "**Required.** 0 = Server, 1 = Client." +msgstr "* * 필수입니다. * * 0 = 서버, 1 = 클라이언트." + +#: ../../Instance_Setup/Instance_Setup.rst:74 7cc185b424ee489282d9d85571d54fae +msgid "``--setup_addr_port``" +msgstr "`` --setup_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:74 37df8730dce9408a87ba48b775eb2b0a +#, fuzzy +msgid "**Required.** Bind address and port (e.g. ``127.0.0.1:8000``)." +msgstr "바인딩 주소와 포트(``host:port``)를 입력하세요." + +#: ../../Instance_Setup/Instance_Setup.rst:77 388a3dd952ac47a78598792ef7d587bc +msgid "``--connect_addr_port``" +msgstr "`` --connect_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:77 b4f319ec304e4a26a09ac62b90db0a07 +#, fuzzy +msgid "Required for Client only. Server address and port to connect to." +msgstr "클라이언트인 경우 연결할 서버 주소와 포트도 입력합니다." + +#: ../../Instance_Setup/Instance_Setup.rst:80 7bb925846a36488d945c3442889d83c0 +msgid "``--setup_num``" +msgstr "`` --setup_num ``" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7f3e0b1925924583acf7acb25ec6e183 +msgid "" +"*Ignored.* The script always launches a single instance. This flag is " +"accepted for compatibility but has no effect." +msgstr "* 무시됨. * 스크립트는 항상 단일 인스턴스를 시작합니다. 이 플래그는 호환성을 위해 허용되지만 효과가 없습니다." + +#: ../../Instance_Setup/Instance_Setup.rst:86 99952fa68b694654b82a309a26419152 msgid "Examples" msgstr "예" -#: ../../Instance_Setup/Instance_Setup.rst:84 13847166f8d54c9db943e1e55cf66c55 +#: ../../Instance_Setup/Instance_Setup.rst:88 9c77e76f40a940e99e754d27e3bc1b05 msgid "**Launch a single server** on ``127.0.0.1:8000``:" msgstr "**127.0.0.1:8000``에서 단일 서버 실행**:" -#: ../../Instance_Setup/Instance_Setup.rst:90 aa92290b83324661a23d2343f7ca56fb +#: ../../Instance_Setup/Instance_Setup.rst:94 37df8730dce9408a87ba48b775eb2b0a msgid "" "**Launch a client** bound to port ``9000``, connecting to a server at " "``127.0.0.1:8000``:" msgstr "``127.0.0.1:8000``에서 서버에 연결하여 ``9000`` 포트에 바인딩된 **클라이언트 실행**:" -#: ../../Instance_Setup/Instance_Setup.rst:97 9d2ca3944f984434b4ecb579b075ccc4 +#: ../../Instance_Setup/Instance_Setup.rst:101 +#: 0bdae3cd584e464ab526840aa032e3a4 msgid "" "**Launch from an existing configuration** (if ``setup.json`` is present):" msgstr "**기존 구성에서 실행**(``setup.json``이 있는 경우):" -#: ../../Instance_Setup/Instance_Setup.rst:105 -#: d88aa64a570f4f36baf5c7c92d4bd861 +#: ../../Instance_Setup/Instance_Setup.rst:109 +#: ff45af42eb7d4d3faa8640a18bfd61f6 msgid "Configuration File" msgstr "구성 파일" -#: ../../Instance_Setup/Instance_Setup.rst:107 -#: cfb0db3d5e23418d9c93ddb2d34548d4 +#: ../../Instance_Setup/Instance_Setup.rst:111 +#: 39e34a4760354d78b87a8e050029e197 msgid "" "The script writes a file named ``setup.json`` in the same directory. Its " "structure is:" msgstr "스크립트는 동일한 디렉터리에 ``setup.json``이라는 파일을 작성합니다. 그 구조는 다음과 같습니다:" -#: ../../Instance_Setup/Instance_Setup.rst:131 -#: 0a2b6c0e312a49de8ed7838e045b3b66 +#: ../../Instance_Setup/Instance_Setup.rst:135 +#: 9e72e7ecd43c498299e24f5598b6f2b8 msgid "" "**Each list contains at most one object.** When a new server or client " "configuration is added, the entire list for that type is replaced." msgstr "" "**각 목록에는 최대 하나의 개체가 포함됩니다.** 새 서버 또는 클라이언트 구성이 추가되면 해당 유형에 대한 전체 목록이 대체됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:136 -#: 1a256391599446d99c3cc375639f251c +#: ../../Instance_Setup/Instance_Setup.rst:140 +#: 8731a083a201487bb579beb4a238a0db msgid "Custom Parameters" msgstr "맞춤 매개변수" -#: ../../Instance_Setup/Instance_Setup.rst:138 -#: 36cefa4820cd42ee93fdb560be1b1032 +#: ../../Instance_Setup/Instance_Setup.rst:142 +#: 1568543fd757484da86f12a72ccb58b5 msgid "" "You can manually edit ``setup.json`` to include any parameter accepted by " "``TCP_Server_Base`` or ``TCP_Client_Base`` (see the source code for the full" @@ -201,13 +247,13 @@ msgstr "" "구성이 삭제되고 새 필드만 저장됩니다. 따라서 사용자 정의 매개변수를 원할 경우 처음 실행한 후에 해당 매개변수를 추가하거나 파일을 " "수동으로 편집해야 합니다)." -#: ../../Instance_Setup/Instance_Setup.rst:150 -#: 6b90a320b39147c7ac5287c51b028da3 +#: ../../Instance_Setup/Instance_Setup.rst:154 +#: c62b1bfe8ceb40f8865a34ac87b4ba18 msgid "Extension Protocols and Startup Mode" msgstr "확장 프로토콜 및 시작 모드" -#: ../../Instance_Setup/Instance_Setup.rst:152 -#: 81cf1090db0240e19aecfb16cdafb622 +#: ../../Instance_Setup/Instance_Setup.rst:156 +#: e6a7dcb4ea184e828e6667ae6505d872 msgid "" "Two extension protocols ship with the launcher and are loaded automatically " "for every instance whose ``setup.json`` entry sets " @@ -216,58 +262,75 @@ msgstr "" "두 가지 확장 프로토콜이 런처와 함께 제공되며 ``setup.json`` 항목이 ``is_extend_command=True``로 설정된" " 모든 인스턴스에 대해 자동으로 로드됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:156 -#: e89028b886064e8ab1e94e5e6a6cdaa0 -msgid "``command_control_extension_tcp.py`` – remote command" +#: ../../Instance_Setup/Instance_Setup.rst:160 +#: 7159f768fc9a4ac199bfe0a4d8478fba +#, fuzzy +msgid "" +"``command_control_extension_tcp.py`` – remote command execution with per-" +"client log collection (``/command``)." msgstr "``command_control_extension_tcp.py`` – 원격 명령" -#: ../../Instance_Setup/Instance_Setup.rst:157 -#: 86a457e4e5bf4791bcbef5bb12612391 +#: ../../Instance_Setup/Instance_Setup.rst:162 +#: d2d277053c74470abc949b891493a5f0 +#, fuzzy msgid "" -"execution with per-client log collection (``/command``). - " -"``forward_extension_tcp.py`` – forwarding messages, files, multiple files, " -"folders and multiple folders to any number of destination clients " -"(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " -"``/folder_forward``, ``/multiple_folder_forward``)." +"``forward_extension_tcp.py`` – forwarding files, multiple files, folders and" +" multiple folders to any number of destination clients (``/file_forward``, " +"``/multiple_file_forward``, ``/folder_forward``, " +"``/multiple_folder_forward``)." msgstr "" "클라이언트별 로그 수집(``/command``)을 통한 실행. - ``forward_extension_tcp.py`` – 메시지, 파일," " 여러 파일, 폴더 및 여러 폴더를 원하는 수의 대상 클라이언트(``/send_msg_forward``, " "``/file_forward``, ``/multiple_file_forward``, ``/folder_forward``, " "``/multiple_folder_forward``)에 전달합니다." -#: ../../Instance_Setup/Instance_Setup.rst:164 -#: baee7200e5634507a28b2f69309c3c49 +#: ../../Instance_Setup/Instance_Setup.rst:168 +#: a8194174e5fd4dcf87ede716eaada9a4 +msgid "" +"Plain-message forwarding is native to the TCP protocol (no extension " +"needed): the client-only command ``/forward_send_msg`` relays messages to " +"the listed destination clients through the server." +msgstr "" +"일반 메시지 전달은 TCP 프로토콜에서 기본입니다 (확장 필요 없음). 클라이언트 전용 명령 ``/forward_send_msg `` 는" +" 서버를 통해 나열된 대상 클라이언트에 메시지를 전달합니다." + +#: ../../Instance_Setup/Instance_Setup.rst:173 +#: 98297b76f3404736b92b6c507726a50a msgid "" "With ``is_extend_command=False`` (the default) only the raw TCP protocol is " "started." msgstr "``is_extend_command=False``(기본값)를 사용하면 원시 TCP 프로토콜만 시작됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:167 -#: f140a881acba4933bd83af5bb35737d1 +#: ../../Instance_Setup/Instance_Setup.rst:176 +#: 9b5ab0319a9843f1b39721d3a96d1777 msgid "" "The ``is_input_command_in_console`` flag selects how the instance is " "started:" msgstr "``is_input_command_in_console`` 플래그는 인스턴스가 시작되는 방법을 선택합니다:" -#: ../../Instance_Setup/Instance_Setup.rst:170 -#: a40aa238baac4503a0dc0c9cea0745ee -msgid "``True`` (default) – ``start_TCP_Server()`` /" -msgstr "``True`` (기본값) – ``start_TCP_Server()`` /" +#: ../../Instance_Setup/Instance_Setup.rst:179 +#: 1ec730c995b1454da1b208331d9ca8fe +msgid "" +"``True`` (default) – ``start_TCP_Server()`` / ``start_TCP_client()`` is " +"called directly and the console input loop runs in its own thread." +msgstr "" +"`` True `` (기본값) – `` start_TCP_Server () ``/`` start_TCP_client () `` 가 직접 " +"호출되고 콘솔 입력 루프가 자체 스레드에서 실행됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:171 -#: fca5c2c0fe914740a931957b30a9927b +#: ../../Instance_Setup/Instance_Setup.rst:182 +#: 3cfb2eba312943cea85388f4e9faf40d +#, fuzzy msgid "" -"``start_TCP_client()`` is called directly and the console input loop runs in" -" its own thread. - ``False`` – the instance runs in a background thread and " -"the launcher keeps the process alive until the instance stops (useful for " -"headless deployments)." +"``False`` – the instance runs in a background thread and the launcher keeps " +"the process alive until the instance stops (useful for headless " +"deployments)." msgstr "" "``start_TCP_client()``는 직접 호출되며 콘솔 입력 루프는 자체 스레드에서 실행됩니다. - ``False`` – " "인스턴스가 백그라운드 스레드에서 실행되고 실행 프로그램은 인스턴스가 중지될 때까지 프로세스를 활성 상태로 유지합니다(헤드리스 배포에 " "유용함)." -#: ../../Instance_Setup/Instance_Setup.rst:177 -#: c2925f0f85154040bcdd1252a228fd7f +#: ../../Instance_Setup/Instance_Setup.rst:186 +#: 3b3482fa903f4afd81234e588a229c1f msgid "" "Both extensions also expose injectable registration " "(``setup_server_commands(instance)`` / ``setup_client_commands(instance)``) " @@ -282,62 +345,67 @@ msgstr "" "is_input_command_in_console=True)``를 허용하는 편의 기능을 제공합니다. 인스턴스이므로 코드에서 동일한 " "인스턴스에 여러 확장을 로드할 수 있습니다." -#: ../../Instance_Setup/Instance_Setup.rst:186 -#: e58290a2c5f3492bafa47f15b7c2958f +#: ../../Instance_Setup/Instance_Setup.rst:195 +#: 1dc7b46c0e3047d2885573cc4f55e7c6 msgid "Internal Operation" msgstr "내부 운영" -#: ../../Instance_Setup/Instance_Setup.rst:188 -#: 6655764b01644b92910338c01d38b0cb -msgid "Each instance is launched in a new terminal window" +#: ../../Instance_Setup/Instance_Setup.rst:197 +#: 8870bc6b731d40a095a40faed0c8f16d +#, fuzzy +msgid "" +"Each instance is launched in a new terminal window (or background process)." msgstr "각 인스턴스는 새 터미널 창에서 시작됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:189 -#: 6824028a99254c77a55fb56d51b5e9a5 -msgid "(or background process)." -msgstr "(또는 백그라운드 프로세스)." - -#: ../../Instance_Setup/Instance_Setup.rst:190 -#: 0a09353648d44ba2a840fc1cf8a850bc -msgid "The configuration is passed via a temporary JSON" +#: ../../Instance_Setup/Instance_Setup.rst:199 +#: 13c720a93ba54e8ca330fce3a078bb5b +#, fuzzy +msgid "" +"The configuration is passed via a temporary JSON file to avoid shell " +"escaping issues." msgstr "구성은 임시 JSON을 통해 전달됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:191 -#: f6327281e35a4304b404563d00301a1a -msgid "file to avoid shell escaping issues." -msgstr "쉘 이스케이프 문제를 방지하기 위한 파일입니다." - -#: ../../Instance_Setup/Instance_Setup.rst:192 -#: 997e364f35714b18b1e47af641ce58db -msgid "If an instance fails to start, the error is" -msgstr "인스턴스가 시작되지 않으면 오류는 다음과 같습니다." - -#: ../../Instance_Setup/Instance_Setup.rst:193 -#: 4404c088296e4c35b49ed59eca3b6678 -msgid "displayed and the window pauses for inspection." +#: ../../Instance_Setup/Instance_Setup.rst:201 +#: 9566eb3cb70e4b77bcbdb584f7e0b6fc +#, fuzzy +msgid "" +"If an instance fails to start, the error is displayed and the window pauses " +"for inspection." msgstr "표시되고 검사를 위해 창이 일시 중지됩니다." -#: ../../Instance_Setup/Instance_Setup.rst:196 -#: 2b8f982006184d36b06d4ac5579a36f4 +#: ../../Instance_Setup/Instance_Setup.rst:205 +#: 5f0f0f6a41384356986a1182f14a0a4c msgid "Requirements" msgstr "요구사항" -#: ../../Instance_Setup/Instance_Setup.rst:198 -#: f10fabd9883942b780cd0822da245837 +#: ../../Instance_Setup/Instance_Setup.rst:207 +#: 49ceb74e41104b00971cd9edecef08ae msgid "Python 3.6+" msgstr "파이썬 3.6+" -#: ../../Instance_Setup/Instance_Setup.rst:199 -#: f464e1a571fa46388937f3236b36d3bf +#: ../../Instance_Setup/Instance_Setup.rst:208 +#: 541cd1be44c044f3880eb220a87c06b7 msgid "The ``network_api.connect_tcp`` module must be" msgstr "``network_api.connect_tcp`` 모듈은 다음과 같아야 합니다." -#: ../../Instance_Setup/Instance_Setup.rst:200 -#: fc6ca227cd904333916e81b4a7093acc +#: ../../Instance_Setup/Instance_Setup.rst:209 +#: 427fef8d7c5047fc934681026969639a msgid "importable (the script imports ``TCP_Server_Base``" msgstr "가져오기 가능(스크립트는 ``TCP_Server_Base``를 가져옵니다)" -#: ../../Instance_Setup/Instance_Setup.rst:201 -#: dcc9c410853247b0a3302c2c41f6fe13 +#: ../../Instance_Setup/Instance_Setup.rst:210 +#: fb35af261d664664bdb3c1b3aaac3e83 msgid "and ``TCP_Client_Base`` from there)." msgstr "및 거기에서 ``TCP_Client_Base``)." + +#~ msgid "``True`` (default) – ``start_TCP_Server()`` /" +#~ msgstr "``True`` (기본값) – ``start_TCP_Server()`` /" + +#~ msgid "(or background process)." +#~ msgstr "(또는 백그라운드 프로세스)." + +#~ msgid "file to avoid shell escaping issues." +#~ msgstr "쉘 이스케이프 문제를 방지하기 위한 파일입니다." + +#~ msgid "If an instance fails to start, the error is" +#~ msgstr "인스턴스가 시작되지 않으면 오류는 다음과 같습니다." diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.add_extension.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.add_extension.po new file mode 100644 index 0000000..233b313 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.add_extension.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.add_extension.rst:2 239fa5edd0d944bd884b21d803e0f4a1 +msgid "PyFlow.add\\_extension module" +msgstr "" + +#: PyFlow.add_extension.copy_extension_files:1 c4a96a18a6754f1fb4c5c3a9ff9dd518 +#: of +msgid "Validate extension path(s) and return them as a list." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst 4b8e1285572947c4a34dbcd9e22fb52c +#: 78eb00a995d84b028e95127f753b4fb5 PyFlow.add_extension.remove_extension +#: dff4aabd6aad43e188c9fc9b73d9142b of +msgid "Parameters" +msgstr "" + +#: 49c2e6f1496d433ab7a9d162802419f1 5de3fc7a955443cb9bef23851780925f +#: PyFlow.add_extension.add_extension:3 +#: PyFlow.add_extension.copy_extension_files:3 +#: PyFlow.add_extension.remove_extension:3 b82537ecca1e4f418a5a6aacacdd900c of +msgid "a single path string or a list of path strings." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst fa91fb96a98f4374b2da5085d1373ef4 +msgid "Returns" +msgstr "" + +#: 450992bbbe91432fb44b0265e58d8f59 PyFlow.add_extension.copy_extension_files:5 +#: of +msgid "The original paths as a list (extensions are not copied)." +msgstr "" + +#: 487ed0fee55540c796c92b88d0a8b2ea +#: PyFlow.add_extension.add_added_extension_logs:1 of +msgid "Append paths to the extension registration log file." +msgstr "" + +#: PyFlow.add_extension.add_extension:1 cd718206d465487ebd43c17e796c4da5 of +msgid "Register extension file(s) in added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.remove_extension:1 bb4313796aff49a1b96a37d66912f18a of +msgid "Remove registered extension path(s) from added_extensions.json." +msgstr "" + +#: 6520ce551e5f40e6afe59c549d15e493 PyFlow.add_extension.remove_extension:5 of +msgid "If the registration file does not exist, this is a no-op." +msgstr "" + +#: 26339cf4e8a041f798d969a890592971 +#: PyFlow.add_extension.load_registered_extensions:1 of +msgid "Load every registered extension from added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.load_registered_extensions:3 +#: e53cceb4fe384c1fa10ddf1905998823 of +msgid "" +"For each registered path, the module is imported dynamically and its " +"``setup_server_commands(instance)`` or " +"``setup_client_commands(instance)`` is called, depending on " +"*instance_type*." +msgstr "" + +#: 11efbd89b0254bb190c21005396f53dd +#: PyFlow.add_extension.load_registered_extensions:7 of +msgid "" +"Raises ImportError if the JSON file is reachable but a module cannot be " +"imported or loaded, or if the required setup function is missing." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po new file mode 100644 index 0000000..8b9998c --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po @@ -0,0 +1,36 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.command_control_extension_tcp.rst:2 +#: a6084f2941f34f579f28c55e9dfb768d +msgid "PyFlow.command\\_control\\_extension\\_tcp module" +msgstr "" + +#: 9209095513ec402980427a0ddd988219 +#: PyFlow.command_control_extension_tcp.setup_server_commands:1 of +msgid "Register the control-extension commands on a server instance." +msgstr "" + +#: 114b0a54c29747fd88bc2852eab174bf +#: PyFlow.command_control_extension_tcp.setup_client_commands:1 of +msgid "Register the control-extension commands on a client instance." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.flow_setup.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.flow_setup.po new file mode 100644 index 0000000..02b1c38 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.flow_setup.po @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.flow_setup.rst:2 a6a54598744740409fe0b28f73834ffe +msgid "PyFlow.flow\\_setup module" +msgstr "" + +#: 586460f36f3b4654abb0db6b5d77b39d PyFlow.flow_setup.launch_web_tool:1 of +msgid "Launch the transfer_web launcher (``kind`` = \"server\" or \"client\")." +msgstr "" + +#: PyFlow.flow_setup.launch_web_tool:3 ef1274ba70124aeeadee3d4cfcca99c2 of +msgid "" +"The web tool is a Flask app that opens a browser UI, so it runs in its " +"own process (a terminal window when one is available, otherwise detached)" +" and the launcher returns immediately." +msgstr "" + +#: 7086887643164f229f919546dceb0e36 PyFlow.flow_setup.edit_existing_instances:1 +#: of +msgid "Vim-style editor to delete/change existing instances." +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:3 d04ffe1cf853403292908fc69962f7dc +#: of +msgid "Returns (status, servers, clients):" +msgstr "" + +#: 5a751731515c4adfba9787fdd5e93215 PyFlow.flow_setup.edit_existing_instances:4 +#: of +msgid "status == \"saved\" -> setup.json was written (:w / :wq); keep the" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:5 e61a6437dae14d1e9f9ac4e6848cd444 +#: of +msgid "returned edited lists" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:6 bb9424a8ea894dbb888df4fa2e0875ed +#: of +msgid "status == \"discarded\" -> the editor was exited without saving" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:7 d0d81a426f754475922c3e9f4aee6627 +#: of +msgid "(:q! / :q) and the original lists are returned unchanged" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po new file mode 100644 index 0000000..a676444 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.forward_extension_tcp.rst:2 +#: 39b0ca283c7c48b08f66a67eb80766b0 +msgid "PyFlow.forward\\_extension\\_tcp module" +msgstr "" + +#: PyFlow.forward_extension_tcp:1 ad6b75f791c543dea48894cd8bac6941 of +msgid "Forward extension for the TCP protocol." +msgstr "" + +#: PyFlow.forward_extension_tcp:3 ff4264b5fba24d7d93e9bb3e398edfd5 of +msgid "" +"Disk-based, upload-then-push forwarding of files and folders to a list of" +" destination clients. This is deliberately a second implementation of " +"file forwarding: the native TCP protocol already streams files and " +"folders in memory (``/forward_file`` / ``/forward_folder`` on a client " +"console, relayed by the server as ``/forward_item`` with no disk I/O on " +"the server), while this extension uploads the data to the server's " +"transfer directory first and then asks the server to push the stored " +"copies. Plain-message forwarding is native as well (the client-only " +"command ``/forward_send_msg``, relayed by the server), so no string " +"forwarding lives here." +msgstr "" + +#: 2f49002886224f5bb68eb09f2c4a8a30 PyFlow.forward_extension_tcp:14 of +msgid "Transfer families added by this extension:" +msgstr "" + +#: PyFlow.forward_extension_tcp:16 ab1c45d0c28345c2898a54c66c98416b of +msgid "/file_forward <(ip, port)> ..." +msgstr "" + +#: 93ef2915b1fa4bd9b4b42e8e6ec747f8 PyFlow.forward_extension_tcp:17 of +msgid "forward one file to every listed destination" +msgstr "" + +#: 9773fb5cd9414f0aabb496ae3a32005a PyFlow.forward_extension_tcp:18 of +msgid "/multiple_file_forward ... <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:19 ee54881259274d6b9085bf71b44d659a of +msgid "forward several files to every listed destination" +msgstr "" + +#: 19144093ca8d45e988dfcfe09221bf6a PyFlow.forward_extension_tcp:20 of +msgid "/folder_forward <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:21 b73d9263c9874b1e9ea5a714138d72d8 of +msgid "forward one folder (structure preserved) to every destination" +msgstr "" + +#: 4f7e8bb4b31c4eddbb78d815e85f8795 PyFlow.forward_extension_tcp:22 of +msgid "/multiple_folder_forward ... <(ip, port)> ..." +msgstr "" + +#: 9e95e860d39744f8ba3d52ddaf357943 PyFlow.forward_extension_tcp:23 of +msgid "forward several folders to every listed destination" +msgstr "" + +#: 612162f14a574132bf396a1b6ea853f1 PyFlow.forward_extension_tcp:25 of +msgid "" +"Items come first, destinations last; every destination is written as a " +"Python address tuple, e.g. ``\"('127.0.0.1', 3000)\"``. There is no limit" +" on the number or size of items or destinations." +msgstr "" + +#: 1b0ec7328493432fb876117b1f76bb3b PyFlow.forward_extension_tcp:29 of +msgid "" +"The commands are only available on the client console: they are " +"registered in the \"client\" handler group, so typing them on the server " +"console is rejected as an unrecognized command. Forwarding goes through " +"the server - the client uploads the data over the normal transfer channel" +" (the server stores it in its transfer directory) and then asks the " +"server to push it to the destinations, which receive it through the main " +"protocol's own receive paths. Destinations that are unreachable (not " +"connected to the server, or the server itself, which is never in the " +"client table) are skipped and the remaining destinations are still " +"served." +msgstr "" + +#: 443d9fe7a21148d984a0e3f2f3cb2c28 +#: PyFlow.forward_extension_tcp.setup_client_commands:1 of +msgid "Register the file/folder forward commands on a client instance." +msgstr "" + +#: 5dbc099afeb54fbb859bf5760331adfd +#: PyFlow.forward_extension_tcp.setup_client_commands:3 of +msgid "" +"Message forwarding (``/forward_send_msg``) is native and needs no setup. " +"Each command binds its transfer kind and single/multiple policy into the " +"shared handler via functools.partial; where_to_run=\"client\" makes them " +"fire from console input only." +msgstr "" + +#: 392556d0850f44b1b64dae0fe65a748c +#: PyFlow.forward_extension_tcp.setup_server_commands:1 of +msgid "Register the file/folder forward relays on a server instance." +msgstr "" + +#: 399725c6728441568702225abc50b2c4 +#: PyFlow.forward_extension_tcp.setup_server_commands:3 of +msgid "" +"The message relay (``/forward_send_msg``) is native and needs no setup. " +"These handlers are triggered by relay requests sent by clients, i.e. they" +" live in the \"server\" group: messages coming in from other instances " +"are dispatched there. The /xxx_forward commands themselves stay in the " +"client group, so typing them on the server console is rejected as " +"unrecognized." +msgstr "" + +#: 337c29db84fe4d3099a2e3d5e135d0d1 PyFlow.forward_extension_tcp.client_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable client (mirrors the control " +"extension)." +msgstr "" + +#: 99ee11955438459d84f5c1ae6f3fedbb PyFlow.forward_extension_tcp.server_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable server (mirrors the control " +"extension)." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po new file mode 100644 index 0000000..8e8a0db --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po @@ -0,0 +1,1759 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_tcp.rst:2 +#: 1e4cd09ce3ef45ceb95b8eb81eb7493b +msgid "PyFlow.network\\_api.connect\\_tcp module" +msgstr "" + +#: 25b88cdb337c44539a843c674ba6825b PyFlow.network_api.connect_tcp:1 of +msgid "" +"TCP transport for PyFlow: the server and client base classes and the wire" +" parsers." +msgstr "" + +#: 1f1482fc5c2849a68f34c74a8b55ab79 PyFlow.network_api.connect_tcp:3 of +msgid "" +"``TCP_Server_Base`` accepts connections and dispatches inbound lines; " +"``TCP_Client_Base`` connects, sends and reads on the same conventions:" +msgstr "" + +#: 76e93df1b95547ff8bded710dc31340d PyFlow.network_api.connect_tcp:6 of +msgid "" +"one message per line, terminated by a newline; a line that starts with " +"``/`` is a command and goes to the command handlers, anything else is a " +"plain message reported to the registered message listeners;" +msgstr "" + +#: 2497ecf21ac947c78502a3839a452cb8 PyFlow.network_api.connect_tcp:9 of +msgid "" +"an RSA-encrypted channel is negotiated right after connect unless " +"``is_enable_encrypto`` is False;" +msgstr "" + +#: 9702905f1f8a4750905b2ffb96f99ec8 PyFlow.network_api.connect_tcp:11 of +msgid "" +"file/folder transfer, message forwarding and port allocation are layered " +"on the same socket and share its command namespace." +msgstr "" + +#: 4cc5743bdfe54148ad38f54f10e688c9 PyFlow.network_api.connect_tcp:14 of +msgid "" +"The forwarding extensions use the module-level parsers " +"`parse_forwarded_message`, `parse_forward_items_and_addrs`, " +"`parse_forward_originator` and `forward_skip_message`." +msgstr "" + +#: 49796529cb7f49ee8131b257212b5420 PyFlow.network_api.connect_tcp:18 of +msgid "" +"Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and " +"``TCP_Client_APIs.rst``; argument, return and exception contracts live in" +" the docstrings below." +msgstr "" + +#: 39274682c4bd46489ed6fe535b50ede5 +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:1 of +msgid "Split a ``/send_msg_from `` relay envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07da14a120314eb7a3acd1c46ddfeaca 0b005c95146946208a01640628f50a57 +#: 10904afb15ee45b29e91a2ab5da0db26 14d9353e52a54c019428fb94211f17f6 +#: 1861e8bcd5bb41489f13265860311349 1c6ba1e0968a409499b7a262c34dcc2a +#: 20b95160870648538fd617ca4ce3d2b5 2351fc654dfc452a8d07e5d566265d9f +#: 27611fb8240a422db411c043f60bdbe9 464f4d62231e4a61b1a939ad2054b13c +#: 48bd66821c14451d9a0c682d9468a5a5 4e1c4be3e22f403396d5ee1788d0e3b0 +#: 4fa9ba6e763440b8ab00832520b4d305 501a0caef71d436e84469bc1ef9c1e5f +#: 52bdf8d502cc4a31a6df5ae543ceb182 531f1e6ff68e44bcac61633e2d7c511f +#: 5404b8d0f66848c3b1db9f08a25f92c0 55fc240d149746e190ecf924a2ca6dd4 +#: 59ab6a93eceb40f7b590133c3d2b8548 6d8d8704b9c94105928dbbdf15ce9f12 +#: 70d6dae11a144ed7b7e9d8e16dbf478c 71de1bb0c4d24062a819a6fe59013d34 +#: 74c64b0da3294322b512f3155955fb7a 76f08e34cd4a42c880880cfb511fcc66 +#: 7a6a9f1497ac48f5867783d78fdae37c 7c3501da0be44d78a671e96ba4384489 +#: 7e9c7fe1ae974c1082550fb3e6e3de1f 7fc19de66098437faef7bffed3b5f752 +#: 7fdf476d99ff4f54ad35cf4bb506e47d 812d9d039fe34f71aa4b662c4511c8e9 +#: 81f8f7d346ff45639f39eb0f033103df 95d8b328540c4094a4fcc8fdc9139645 +#: a163536aaa1f49399502da52fb481666 acdf8b3b266f4eccb6ceca17a110603b +#: b097aecf6ae74e17b36fc9e806d5b26a b3db16a6c2374980ae9b071d9f3f15e6 +#: c2a18448d83f4879bcb29f51ca31bc5b c5ed2382d01042c68a5e372a4c7de2ac +#: d36c8c10afba4732969367886b8663ab d6d746e16dbd47308eea5aacd9614f15 +#: d8e94668c5d7434fa61c2ffcbe73b6af dad1324e962d49419245fe3b88c20121 +#: dd3d356c14cb462e98ea42e3502dca80 eb24a57758194ab2bdef9e95362584df +#: ef5da8d6c91c469fad0871d213999d2a ff3615b7096e46bba8797b739bb954b2 +msgid "Parameters" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:3 +#: e42a6166c82d4861b25cb55f587db28c of +msgid "Received line, e.g. ``/send_msg_from ('127.0.0.1', 3000) hello``." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07f0651994f343d7ac8ee26ae4a45c8e 0b14e887dc524d55affce5a3bd9b9f8d +#: 0cb557ccd8a6423091d0d562f1af31fd 0d1ef94754104f01a11302d72600e73a +#: 1475f9cb68fe417b9de7226406e810b5 14dcda8b059b480982e3fb03399e665b +#: 1d7731490c2f49959618924ddf4327fd 1e40a9184f8544c6b7a1472d55c66a11 +#: 24af9c4562134d16b50c664902b39dff 295718cd92e04380af38e7afaff04010 +#: 34fc7f7760d94c2f8bac262ff821b4d7 451b693e9c414cff9798a5a1592fe9cd +#: 47bbee4bd36c4f998fd04c8cc8d9c199 537ed27e714148da8ea804e3562ccd96 +#: 565b9def256a453784c749fe2ee93bc8 57dee3931014419c91d049fca185ef1b +#: 5dcfbd3c4b13401a82e19373602a4b92 635677a7aff64b79a4f69ffb4c22841c +#: 74b8a4ad713a43f2a14faf494e1886a4 7eca27378af64ce99374c606219ff337 +#: 7f6560a3959c4f069adff9d00ca33f54 855ad029af764579bf4ab14cbd430caa +#: 967377f151e84130a9778ece6228bec2 99035a24f6e94207b84545f8c71a451e +#: a90e5d6a51cd4591afeb3cd934071de0 c72891376fe2481d95c7d9ae3014ae47 +#: ca270cee6d3f4eb7ba331ab841c77f38 cb62202c244d4c08974c674c13a02dca +#: db47465f222c4f3db50a9133de7035f5 eb66bb58c7534346b105203e37b25760 +msgid "Returns" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:7 +#: f25d1962656c4f339805427e2573f84f of +msgid "" +"``(sender_id, payload)`` where ``sender_id`` is the sender's " +"``\"ip:port\"``, or None when the line is not a well-formed envelope." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:9 +#: d0d7af37e4134902ae64e10b2e43b4c1 of +msgid "``(sender_id, payload)`` where ``sender_id`` is the" +msgstr "" + +#: 09b385f0a98640c981d8563427e44d7e +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:10 of +msgid "" +"sender's ``\"ip:port\"``, or None when the line is not a well-formed " +"envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 0acb7e2291644625b8873e997a801b95 140c4a59eb0f43d5b4dc46fc2d3348a7 +#: 1412375394164f17aa5b40b7d9ecf0a4 19eb6fca79324b0791f8ec7488a5246c +#: 21c016ab955e4e6499a97f0ca86684cb 27bd312392a2404099fa44fd8787ed28 +#: 27dcdca3b6b247069ddce775ec471c70 28f7c9db48d949ef930abcedef021138 +#: 28fa8dcc5913460e8906e926c95408b9 2e363ed3ce6746c08ebfbf8bb889b623 +#: 3d654cd4b6b044baa3774f758d61da71 48e6af3ee30f47409e2837867a01ff48 +#: 50da78ed5f904c74a876d7b494241efa 50df226290614747b5568f633a883ae0 +#: 564af23329ae4b29aaea3a3638292f42 571f3b412784479a9bfdcb5034c6a39f +#: 6759afd1efe445adb4970c6265c95995 73e666bc0d164cb6a1506517d9acfdb4 +#: 752e44acb3104f9bbf4d3ff7c8bc244b 76862b0024bd42e2b012c999d92e6969 +#: 7ba45226839e40c8a00e684e7c4e07b9 7cd20d28b6794f34a0f53570fea9546a +#: afead614116b49619f1d29738a18e166 b443d67ba649468ca55f1889f18dd006 +#: bbc7b22bbf5e42f7bae149f481de9f3a c58a75656c6143f8a0cb397f24b619b2 +#: d4f12b17df174b038d554053e034cd2a e669e2ac035448fab6479e209ab51c4d +#: f278b3632fbc43f2b4cbeb7759606a68 f6ee270c707f4805b9035fa772078d19 +msgid "Return type" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:1 +#: f22254d8ca2d47ad8bc9c98664369d61 of +msgid "Split forward-command tokens into items and destination addresses." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:3 +#: b207f60051004834bab91ba392a88437 of +msgid "" +"A token of the form ``('ip', port)`` is a destination, everything else is" +" a forwarded item (message text or a path). Used by the native message " +"forwarding (``/forward_send_msg``) and by the file/folder forward " +"extension." +msgstr "" + +#: 5329fb72f5954768a6cec05ff4cccfea +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:7 of +msgid "Tokens after the command name." +msgstr "" + +#: 2aebf669048046ddb279a935bcfcfde4 +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:10 of +msgid "" +"``(items, addrs)`` in the order given; ``items`` holds texts and " +"paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: 96b96bd19fe94312bddf340fad073cbc +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:12 of +msgid "``(items, addrs)`` in the order given; ``items`` holds texts and" +msgstr "" + +#: 8e82954621d748e2b412952a7fb2753b +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:13 of +msgid "paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:1 +#: a51324658c7943158d90ed706cecc41d of +msgid "Build the console notice for a forward destination that cannot be served." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:3 +#: b541e8446e374bc8b38cd7912a1fa33c of +msgid "Destination ``(ip, port)`` that is unreachable or is the server itself." +msgstr "" + +#: 10e03f21d05e478d9cde0c7176f6e1b8 +#: PyFlow.network_api.connect_tcp.forward_skip_message:7 of +msgid "One-line notice for the console." +msgstr "" + +#: 8005e532fe9d491ca8ddd996889d605d +#: PyFlow.network_api.connect_tcp.parse_forward_originator:1 of +msgid "Extract the originator's ``\"ip:port\"`` from a received transfer command." +msgstr "" + +#: 2cf3052a89c645e782430f1f05fb37a1 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:3 of +msgid "" +"The server's forward relay tags every pushed ``/file`` and " +"``/file_folder`` command with the forwarding client's address tuple; a " +"direct send carries the receiver's own address instead." +msgstr "" + +#: 335f6c1b16104395ba38a33188943d8a +#: PyFlow.network_api.connect_tcp.parse_forward_originator:7 of +msgid "Received transfer command." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:9 +#: e5bd1024ad3f47f19cafc4c83a844b60 of +msgid "" +"This instance's own ``\"ip:port\"``; a command carrying it is a direct " +"send and yields None." +msgstr "" + +#: 65469492f844448fab269b701cbdb704 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:13 of +msgid "" +"Originator ``\"ip:port\"``, or None when the command carries no " +"originator (direct send or non-transfer command)." +msgstr "" + +#: 04a0882a3a5e4099a114728b4d1c79c0 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:15 of +msgid "Originator ``\"ip:port\"``, or None when the command carries no" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:16 +#: a2d34276c4f54c69981f9b51f36cc31d of +msgid "originator (direct send or non-transfer command)." +msgstr "" + +#: 7297b41c47664243b9677b32e21de07e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:1 of +msgid "TCP server: accept clients, dispatch commands, relay messages and files." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:3 +#: f5119fd32b60425bb33e4aa148ff7139 of +msgid "" +"Each accepted connection is served by `handle_client` in its own thread: " +"a line starting with ``/`` goes to `handle_command` (built-in commands " +"plus the handlers registered with `register_command`), any other line is " +"a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``." +msgstr "" + +#: 2870b0163d68487daf7526e91eb49ff6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:11 of +msgid "Address the server socket binds to." +msgstr "" + +#: 007c3d5fac544fd480ac10cc56c2f236 0929a7309fef40cab69e9dc22c376ef2 +#: 2e995e9363c54a7ba267ff3a53e10908 507062a8930e432dbcab31b039724b27 +#: 66f4496879ee419d8d748faf8cff0f05 75fcffd8750942559c93c5e590a10503 +#: 83c05847c82b492090a9279f5072434d 8e64315489764ceba6e1120da4675ffb +#: 9c28c79f3bb84af98029a17c0ffc84cd +#: PyFlow.network_api.connect_tcp.TCP_Client_Base +#: PyFlow.network_api.connect_tcp.TCP_Server_Base +#: cf754e752b6c445989b3301b807fc9eb d61fb222783b4f898eb1e757631a1c9a of +msgid "type" +msgstr "" + +#: 2dcb98253fbf428b96fdb5b720769c02 568be916ae0340289a6569e718e7cbf2 +#: 904732a26bcc4835bd3429f0015ae8a4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:14 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:26 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:13 of +msgid "str" +msgstr "" + +#: 6ac74cca7b0f443ca89f3d2c3bbe1aaa +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:17 of +msgid "First port considered for binding and for allocation." +msgstr "" + +#: 0f9991451920470fa3c8a74228b46bb0 9ecf88c5193c470b98dc95d184c94f92 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:19 of +msgid "int" +msgstr "" + +#: 6746407aac194c4880c7cbf82b7fa2fb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:23 of +msgid "" +"Accepted connections keyed by ``(ip, port)``; each value holds " +"``socket``, ``address``, ``id`` and ``connected_time``." +msgstr "" + +#: 7ba2aaf3fe4c42739121eb3ca2bcc79a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:26 of +msgid "dict" +msgstr "" + +#: 93556efee2aa445684af3fdc1532548a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:30 of +msgid "True while the accept loop runs." +msgstr "" + +#: 0ab0667a38254203929e173ab5024f1d 76745b91cce345b092b4e5d931d71126 +#: 839584ae46a0488a95917b35e253768a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:38 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:44 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:32 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:38 +#: f692af2051ae43cdae389a97cd91e9d7 of +msgid "bool" +msgstr "" + +#: 4ad873e4e9da4574bc068029455f62ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:36 +#: fae1c5fe3a844b69a2c7ec611073ecf9 of +msgid "Whether the RSA channel is negotiated." +msgstr "" + +#: 37e6dd691f544a80b9e0949883a2e5a8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:1 of +msgid "Create the server and, unless extended, start accepting clients." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:3 +#: bf58d54a167d45de9a18641877ada912 of +msgid "Address the server socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: 2f4d0c4bfd8246d4984d4c843510775d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:6 of +msgid "First port to bind; also the base of the allocation range." +msgstr "" + +#: 48b1e43c9f2a4e81a9c760ca0d80bad0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:8 of +msgid "Maximum concurrent clients. Defaults to 10." +msgstr "" + +#: 8786a9d2b6b449209be813778d785712 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:10 of +msgid "Step between candidate ports. Defaults to 1." +msgstr "" + +#: 15c0118df8da4454a7fea8d529989237 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:12 of +msgid "Number of ports per step. Defaults to 100." +msgstr "" + +#: 973382c423524d44a569c318399e0b99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:14 +#: d5f8f341d98d478289d9a226f37c1003 of +msgid "Concurrent file transfers allowed. Defaults to 10." +msgstr "" + +#: 4fe9a7607dbc4eebbce4e62f9306c76f +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:17 of +msgid "" +"Reserve a port range across processes, so several instances on one host " +"do not collide. Defaults to False." +msgstr "" + +#: 8aa56addc6ae4a7790dc957d1e1c2b60 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:20 of +msgid "Start the console command thread. Defaults to True." +msgstr "" + +#: 242566ff64584fba88e4fed8bb189110 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:28 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:23 +#: b6d5c80f7a8e4358889f1d90f9a579a4 of +msgid "" +"Worker slots for `submit_task` and threaded command handlers. Defaults to" +" 10." +msgstr "" + +#: 87d1cd7d6569498b8af8bc7064235784 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:26 of +msgid "" +"When True, do not call `start_TCP_Server`; the caller starts the server " +"when ready. Defaults to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:29 +#: ac43634ddb1a4d15a9f3d0f58d011441 of +msgid "" +"Negotiate the RSA-encrypted channel for every connection. Defaults to " +"True." +msgstr "" + +#: 1ffccca56902418ba98ee0c6776ad8fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:37 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:32 +#: e0eb36563dc5479a92ce7555eb5a7965 of +msgid "" +"``[pub_key_path, pvt_key_path]`` pair used instead of the default key " +"lookup; an invalid pair is ignored." +msgstr "" + +#: 420bbdf8d6bb400fbd3548bea03354ac +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:35 of +msgid "" +"Buffering ceiling in MiB for the in-memory forward pump; past it the " +"uploader is told to pause. Defaults to 2048." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 012113b84d6e439db2b772b810838e0a 57443c71b0b84d20b3cf755ee7118632 +#: 63ab5a2f2327475ba324e245f8e6e2ac 7cfe1a15e78d4c2d91a46599dc106667 +#: 8b33b297e4e64f2ba7463207ef17cc68 a4cb07de3c4d4a5c959c403938ed51e6 +#: a85e930c77364c9eaac1c8c9e9e2c740 c79723467b3a4fa1a7ddcb1038fd8414 +msgid "Raises" +msgstr "" + +#: 37d7580e55904c278f12fe0f31fe0d3d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:46 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:39 +#: ed59ec69b9a1430597e6d8758051aff5 of +msgid "" +"If the ``.Flow`` directories or ``decode_command_table.json`` cannot " +"be created or read." +msgstr "" + +#: 810bc26336b24ad1acba3f56d466f8e9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:1 of +msgid "Reserve this server's port range under the cross-process lock." +msgstr "" + +#: 5540b6efb5864c188e080ec7aad5c3ca 9e68e81f7a184af8a8fe90557dbaa5de +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:3 of +msgid "No-op unless ``is_hand_alloc_port`` is True." +msgstr "" + +#: 356ed2e8333d49b98ed6834612b4b56d 4a28dc37e6b74154b3790646ec8abfa7 +#: 5e267cb8af8d49e38ddaeabd8a7e9b57 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:6 +#: daf116d5b2264b1a88c836c1ad5192a3 of +msgid "Step between candidate ports." +msgstr "" + +#: 28bfef7616a1498db2b8827758175f68 7e663e1ae24640d1b856b1f9a0525380 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:8 +#: e18848eedc5d4d8abd97d73a0984a5d3 f81918ab77304aeb99302710e3062db2 of +msgid "Number of ports per step." +msgstr "" + +#: 11e0523d1b194702a07a92748468da25 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:1 of +msgid "Release this server's reserved port range." +msgstr "" + +#: 801c063c5fa447bda1dbf7f5ccbc6a9a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the server port range for this process." +msgstr "" + +#: 672bb7a9496c43ac8b706a3d0062ddfd +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:1 +#: of +msgid "Report whether the server port range is reserved by some process." +msgstr "" + +#: 19c63440e9ff4dadb6a3614ef17ef055 39423e052b70403ba07a713aa9e3cb4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:3 +#: of +msgid "True while the lock file exists." +msgstr "" + +#: 5b6cf828ffb24e098a0c86a09818da73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_unlock:1 +#: of +msgid "Remove the lock file that reserves the server port range." +msgstr "" + +#: 479bdf5518dc4a029ecf47ad44bdcd5d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:1 of +msgid "Allocate the next free server port range and record it on disk." +msgstr "" + +#: 34c085a1bfca4dedafc749bfdcf14ad5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:3 of +msgid "" +"``port`` is moved past the ranges already recorded by other servers, so " +"the instance ends up with a range of its own." +msgstr "" + +#: 197b30e318d741c2b1f8afc925e91e5c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:11 of +msgid "If the server port info file cannot be read or written." +msgstr "" + +#: 1c97c8ef484b42ab86d36b3742bf3879 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_free_port:1 of +msgid "Drop this server's entry from the on-disk port range record." +msgstr "" + +#: 9fc07e857fa048e2956e64bf78f6e386 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:1 of +msgid "Allocate a transfer port, waiting until one is free." +msgstr "" + +#: 505d7b9fb1024bc8ba886e5b0a38fbf2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:3 of +msgid "" +"Allocated port, or 0 when allocation is disabled " +"(``is_hand_alloc_port`` False)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:5 +#: e85fdf7f7a594e0b9aed565b4a3ab42f of +msgid "Allocated port, or 0 when allocation is disabled" +msgstr "" + +#: 153357eb91794cb692467afe5e94b41c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:6 of +msgid "(``is_hand_alloc_port`` False)." +msgstr "" + +#: 26c5c3768e1446e78c9efc5f8038b23d 280a1814abb049cbb26998bd681609ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:1 of +msgid "Release a port obtained from `palloc`." +msgstr "" + +#: 0e0379de669843778f6827973c29372e 3ee789104c4749b1a5749730be83a046 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:3 of +msgid "Port to release." +msgstr "" + +#: 6a8d4efd9f9a45e399525e2f169035ee +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:1 +#: f9203c3ebcb24fd0b5110110b8693b9c of +msgid "Allocate the next port above the base, or the first free one in range." +msgstr "" + +#: 73e9e2ddd451402a93d10dbac6ac9374 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 9d96a242eab8433ca750d670a5f63b82 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:5 of +msgid "Allocated port; None when the upward range is exhausted; 0 when" +msgstr "" + +#: 7b05e18fccbe4ab081d3cd22d59a48e0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:6 +#: ebba1d1151cc4d5991f783986cd0f480 of +msgid "allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 5f15d2ac36eb47a2b23fb93de93755c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:1 +#: f2d9abe435cf49f6825bb72a68013934 of +msgid "Release a port obtained from `file_palloc` and step the cursor back." +msgstr "" + +#: 6c1fb07a212d4bf19717407b0c7184d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:3 +#: c933ad6d68ec4c5d9bb5afc88c386fdd c9edcb78694a444fb0f2cc5f72db01fa +#: e32e165133154b9791e35f117f8a85a9 of +msgid "Port to release. Ignored when allocation is disabled." +msgstr "" + +#: 1afd81c685234a20aa41c5cde93d327d 7f5db505a5fa4c03a56e9ac5ff6aba74 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:1 of +msgid "Allocate the next port below the base, or the first free one in range." +msgstr "" + +#: 3fc63f6d51aa4c0fb22fd0f945f9cfd7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 74a441ccfa5e421dadd215ef0725a25f 9b5089cb85494c069d408e344db75d9d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:5 of +msgid "Allocated port; None when the downward range is exhausted; 0 when" +msgstr "" + +#: 183b4335eebc41058759e30b97477018 9c4b9bddd98c463f9e140bd2d8425dad +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:1 of +msgid "Release a port obtained from `spy_palloc` and step the cursor back." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:1 +#: c66c76cab8564da4b6b6cb855fd63fd5 df9eb5ed5e844a6a8f00e77be5c032e3 of +msgid "Register a custom command handler." +msgstr "" + +#: 85787fb3d66340eb9920983dd95e03c6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:3 +#: cdec2f24d2dc47bc8c505b7472dc3bf7 of +msgid "" +"Command to intercept, e.g. \"/my_command\"; matched case-insensitively " +"against the first token." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:6 +#: c7c29a3836c9493e9c8804cc5896144a of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back to the sender as the response." +msgstr "" + +#: 7c64d0c0b9ac44b995811a0fdaa0dfaf +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:10 of +msgid "" +"\"server\" for commands arriving from clients, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 7d2a7d91ad9743b29110a43100444097 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:13 +#: a0c074a9da664c5eb5fa63deb6765c36 of +msgid "" +"Run the handler on the worker pool instead of the reader thread. Defaults" +" to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:17 +#: ce0ad78e00b24bb8b675ce8d715b8323 e6c616f887334cb6925c03de2ad6f64c of +msgid "" +"False when ``where_to_run`` is neither \"server\" nor \"client\"; the" +" handler is then not registered." +msgstr "" + +#: 3a440bf70b36447e82577535a85baca1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:19 +#: f125f1386b434f3a8199b1ceb8c44eca of +msgid "False when ``where_to_run`` is neither \"server\" nor" +msgstr "" + +#: 1a68dac068db434eb76e28015005f29c 7c7784e8bc9f4a76b12b6be2d3df9285 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:20 of +msgid "\"client\"; the handler is then not registered." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:1 +#: ef5d20f1a16945368383a8cdbc3daa19 of +msgid "Register ``listener(client_id, message)`` for every inbound plain message." +msgstr "" + +#: 12cdf8300e934ace8498d00d39501ba2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:3 of +msgid "" +"Plain messages are the lines received from clients that do not start with" +" ``/``; commands go through the registered command handlers instead." +msgstr "" + +#: 9ec5fafe8ded489785400e3e489b8573 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:6 of +msgid "" +"``listener(client_id, message)`` where ``client_id`` is the sender's " +"``\"ip:port\"``. It runs on the receive thread, so it must not block, and" +" exceptions raised inside it are swallowed." +msgstr "" + +#: 3a640e40aeeb43938e65b49a2cd3dbba +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:1 +#: edb8edc2eb3f433186b54807a8ac43ad of +msgid "Unregister a listener previously added by `add_message_listener`." +msgstr "" + +#: 0336bd7428fa4f688cce21bb6a4156fe 6fd7bb75a29e4973ac2bd05119255039 +#: 977594c4b1074688958a018c03d9ee5b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:3 +#: b59375740438476aa73d6f19c2240ce6 of +msgid "Listener to remove; an unknown one is ignored." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:1 +#: cf95477e5794404fbdf4e877a132ba1c of +msgid "" +"Register ``listener(client_id, full_path, name, size, command)`` per " +"saved file." +msgstr "" + +#: 9c57d09f6d3d437890ae3ad306071701 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:3 of +msgid "" +"Fired after a file uploaded by a client (a direct send, or a forwarded " +"file/folder item staged on the server) has been fully written to " +"``file_transfer_dir``." +msgstr "" + +#: 6c938b7c8be3464f9925527b6deb4e1d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:7 of +msgid "" +"``listener(client_id, full_path, name, size, command)``; ``client_id`` is" +" the uploader's ``\"ip:port\"`` and ``command`` the wire command that " +"triggered the transfer, so a listener can recognise protocol pushes such " +"as ``/crypto_pub_key``. It runs on the transfer thread, so it must not " +"block." +msgstr "" + +#: 549734db517346ec814fa83ec2f46f00 57b1e98125974187b986386da7991dd0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:1 of +msgid "Unregister a listener previously added by `add_file_listener`." +msgstr "" + +#: 58f1ccd078ea406dab00d6d4be886a76 7a6c25f715d14b0aaf4ec7009db9fc3c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:1 of +msgid "Run a callable on the instance's worker pool." +msgstr "" + +#: 6e05c7f46b244ae195c32ee52797fa37 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:3 +#: a51df9d8fc924d468d8f6d8a61dfff12 of +msgid "Callable to run." +msgstr "" + +#: 6f9eee14b6cb4daf82acfbba1e34e873 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:5 +#: e1c79706c9414895bdf7bf183bf5d41a of +msgid "Positional arguments forwarded to ``func``." +msgstr "" + +#: 9221436fa83a4831942cf5be4e37e2cb 993bdb8fbe3240d3b1032014576182fe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:7 of +msgid "Keyword arguments forwarded to ``func``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:10 +#: a6b26d6cf54d4fafac2950cbe33ba56f df538e45ae7f4bd19398fd3d063a4744 of +msgid "" +"Handle for the submitted call; its worker slot is released when the " +"call finishes." +msgstr "" + +#: 214ffa9d5cb242d1b7a5f76c0da16cd6 60f19983f7384aefbb6c97b84dd25f93 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:12 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:12 of +msgid "Handle for the submitted call; its worker" +msgstr "" + +#: 942e2e90fc904344b71b0c2d9c39c9f6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:13 +#: eadf6af0a0954798933dc427ba8e107e of +msgid "slot is released when the call finishes." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:1 +#: a2284b241a28498892e5a59158961b2e fece61298a354d6db119309b29c6751d of +msgid "Start a temporary listener for a side channel (not the main protocol)." +msgstr "" + +#: 01d42ba32c2b41f482dd077cfb950f9b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:3 +#: b7455f1055b7459ca8eee2b5052eb808 of +msgid "" +"``handler(client_socket, address)`` started in its own thread for every " +"accepted connection." +msgstr "" + +#: 5e1b84445ee34c9cac00e1a2c9ef297b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:6 +#: c76bc24facf8407babf9023ef19c3879 of +msgid "Port to bind; None allocates one with `palloc`." +msgstr "" + +#: 4855e3ca5a3a41dfa37eb32a8d6c8d9c 9c69c4dd1d904078af48c6308ba35895 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:8 of +msgid "Listen backlog. Defaults to 1." +msgstr "" + +#: 29148f4a3160460f8dc27fd1e64c0a31 47f98fd15e6144bd845d175cd039a6c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:11 of +msgid "" +"``(port, thread, stop_event)``; setting ``stop_event`` ends the loop," +" which closes the socket and frees the port." +msgstr "" + +#: 91f803f6aa944d07a57cff61d1071efe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:13 +#: ecf3d32c44db40a8a13545d623ffe2c9 of +msgid "``(port, thread, stop_event)``; setting ``stop_event`` ends the" +msgstr "" + +#: 6ca9f2a625634468a15171916ec6d6ea +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:14 +#: abce9546b7444c6383c9020c4133ec63 of +msgid "loop, which closes the socket and frees the port." +msgstr "" + +#: 04767fd6d11641ac88c41c78867609f5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:17 +#: a270cfabdac64296b8a33ad4d662896f of +msgid "If ``port`` is None and no port can be allocated." +msgstr "" + +#: 031d3e96394c454093561375a377dcce 7e534c863e4a4275adf9a3a93dd5cbc0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:1 of +msgid "Open a temporary outbound connection for a side channel." +msgstr "" + +#: 112fda54e1eb4981b4272df61c5416e2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:3 +#: f9f4af8e25654b4ca38700b9faf635ef of +msgid "Host to connect to." +msgstr "" + +#: 26edcce3a1c24d5d95ca0b2a86cdff36 59d3c414b4304673be5d3232d2291680 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:5 of +msgid "Port to connect to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:7 +#: c5766672f68f47c9908e68d2ed3be7f3 of +msgid "Local port to bind; None lets the OS choose." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:9 +#: bfc1b713e9fc4fe797e50c357f472e76 c0e20701686a4ea68dcce54795822508 of +msgid "``on_data(data, client_socket)`` called for every received chunk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:13 +#: c563d04fcd7a41f1a2aeace42974430a cbd0df6f67a64a5580227be38a10020a of +msgid "" +"``(client_socket, thread, stop_event)``; setting ``stop_event`` ends " +"the receiver thread." +msgstr "" + +#: 0e1b9679f3104a9caab601e6bc7c0905 4fbec161e0c84ef1ba95379314846158 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:16 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:15 of +msgid "``(client_socket, thread, stop_event)``; setting ``stop_event``" +msgstr "" + +#: 14528f9f11144ced9b5822d2640fe2a9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:16 +#: c76fce92c3f944c1857df89bbc134540 of +msgid "ends the receiver thread." +msgstr "" + +#: 5ea5fd66c3d3434292e4d8e1dd1da094 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:1 of +msgid "Send one message to every connected client." +msgstr "" + +#: 32945f04e0564f9d88495506f0ab4f90 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:3 of +msgid "Clients whose send fails are disconnected and removed from ``clients``." +msgstr "" + +#: 00de0b1f60b14189b1629f8146db68a6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:5 of +msgid "Payload passed to `send_message`." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:7 +#: a91c8f6921c7435e990516c2134efcf8 of +msgid "``(ip, port)`` to leave out, typically the client the message came from." +msgstr "" + +#: 9d57c62f1fd94c1dacfe32aa50cc9665 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:1 +#: of +msgid "Send the messages of a console line to the clients named in it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:3 +#: e7fbf4bbd84b4920a2b9591fabcaf2d9 of +msgid "" +"``/send_msg`` line as typed: message text followed by one or more ``(ip, " +"port)`` identifiers; each message is delivered to the identifiers that " +"follow it. Addresses that are not connected are skipped with a console " +"notice." +msgstr "" + +#: 9d4819fe4ca94a1b81c9e0423ff98212 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:1 of +msgid "Write one line to a client socket, encrypting when the channel is up." +msgstr "" + +#: 1c7b88e29a0143a39e13441769186ef4 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:3 of +msgid "Target connection." +msgstr "" + +#: 2d32d96ad70e41d893b15bfa90c0864b 6f8bd9adaf2b4c44a9446b049144fe21 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:5 of +msgid "" +"Payload; a str is stripped and newline terminated, bytes are sent as they" +" are." +msgstr "" + +#: 5c03983cb460497fb72f126c1d8a2bc1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:9 of +msgid "" +"True when the payload was written, False for an unsupported payload " +"type." +msgstr "" + +#: 0b11a149117c4a1fb6bcf2fba3d1a515 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:11 of +msgid "True when the payload was written, False for an unsupported" +msgstr "" + +#: 602950a9f5ad48ea92ab60aea3b26488 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:12 of +msgid "payload type." +msgstr "" + +#: 149f767d72dc4da7b1d88fed768d2474 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:15 of +msgid "If the server is not running or no socket was passed." +msgstr "" + +#: 07dac2d20d7041779c8b189d45ee2e64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:16 of +msgid "If the socket write fails (the original error is re-raised)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:1 +#: caf2377ee7f348ab8c95bf2a27c44be4 of +msgid "Read up to ``msg_length`` bytes from a client socket." +msgstr "" + +#: 214af2667d0f443eb16c0a4aaf723b2e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:3 +#: cf94d4a352744a61a1f781128fae9694 of +msgid "Connection to read from." +msgstr "" + +#: 2b0cd669c9994324be7570e59a04677d 7c3eb31687804ba5a59acc140ba50230 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:5 of +msgid "Maximum number of bytes to read." +msgstr "" + +#: 51eade0145d74a2bbfc51070bc779703 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:8 +#: ad9dfdd3ef56445181d7204b7b5bfa15 of +msgid "Received bytes, empty when the peer closed the connection." +msgstr "" + +#: 7103cfcec26c44999cb5464251ee461c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:1 of +msgid "Serve one accepted client until it disconnects." +msgstr "" + +#: 65f9de7f44774aa6aef81fcbe4dc1c64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:3 of +msgid "" +"Registers the client, greets it, announces the encryption mode and reads " +"lines until the peer closes: commands go to `handle_command`, plain " +"messages go to the message listeners and to ``messages_dict``. Runs in " +"its own thread; the client is removed from ``clients`` and the socket " +"closed when the read loop ends for any reason." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:9 +#: db3915757e2842c8afee97b2d471411e of +msgid "Accepted connection." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:11 +#: fae378e888be4821a14ed2483cfde1e0 of +msgid "Peer ``(ip, port)``; used as the client id and as the key in ``clients``." +msgstr "" + +#: 32886e2babcf4283924034c600453c4a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:1 of +msgid "Dispatch one command line received from a client." +msgstr "" + +#: 8972b8c4edf149fd9a4008832c85ed31 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:3 of +msgid "" +"Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, " +"``/crypto_mode``, ``/file``, ``/file_folder``, " +"``/server_file_transfer_port`` and the crypto exchange lines) are handled" +" here; any other name goes to the handlers registered for the \"server\" " +"side via `register_command`. An encryption-mode mismatch closes the " +"connection; an unknown command is only reported on the console." +msgstr "" + +#: 208294924333477a8e7d65aa3130777e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:10 of +msgid "Connection the line came from." +msgstr "" + +#: 306e5026cfc1441d8c754f0e02866cf3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:12 of +msgid "Peer ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:14 +#: a2a3a1fd57e14260a53675457fdff0b4 d87b7a9e04464b7ea4f979d46b42932b of +msgid "Line including its leading ``/``." +msgstr "" + +#: 8d727a2aaab14beaa1b6858b59b0821d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:17 of +msgid "" +"Response for that client, or None when no response is due (crypto " +"lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:19 +#: ed04a3bf285b4f57bb6a5de0c4f7284f of +msgid "Response for that client, or None when no response is due" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:20 +#: c4ca8e9c3e9941e1b33b5e0af2c13bab of +msgid "" +"(crypto lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:1 +#: a784dfdc5ded411d985426e36f6e5435 of +msgid "Send one plain message to a connected target, tagged with its origin." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:3 +#: b545977244dc402aa8586264d2d933f3 of +msgid "" +"Public API for forward extensions: the message is wrapped in a " +"``/send_msg_from `` envelope so the receiver can " +"attribute it to the originator (see `parse_forwarded_message`)." +msgstr "" + +#: 8ee5275fbf894581ade9e6f30d5d4aff +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:7 +#: ee97948978e64b099947a0716f27d581 of +msgid "Destination ``(ip, port)``." +msgstr "" + +#: 38ffd8a7079d4794811e3504c01052f8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:9 of +msgid "Payload to deliver." +msgstr "" + +#: 3b48d6e3b84b4dde9a09f3c2f07c8b61 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:15 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:13 +#: cf0f9590c44e40309471c70ebd2e433d e2207f0b24bf42c984505e0de8c8f8bf of +msgid "Originating client ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:14 +#: d2471a90e269464c977239f406051120 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the envelope was sent." +msgstr "" + +#: 1ae8a7060df7423895175f210441a399 320d99b8923f4eaa886ffe0a16bfc6d9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:24 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:16 of +msgid "False when ``target`` is not connected (a console notice is" +msgstr "" + +#: 1a97f59b270f404096d9c2b37ab09a8c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:17 of +msgid "printed); True when the envelope was sent." +msgstr "" + +#: 6baab06dc12e4ecc80d1a16c26c2a19a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:1 of +msgid "Build the tagged wire command that pushes one forwarded item." +msgstr "" + +#: 0c3f9dcbcf8f413fbd5befefb960c402 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:3 of +msgid "" +"Public API for forward extensions. The originator tuple sits before the " +"trailing transfer id, where the receiver's existing parsers ignore it and" +" `parse_forward_originator` recovers it for attribution." +msgstr "" + +#: 3cf58937a2d04172bf544119279d85c1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:7 +#: a5ccb57e63d144c2844910f8ecf2faa4 of +msgid "\"file\" or \"file_folder\"." +msgstr "" + +#: 621a93603c8549c0ba047bee6aae5a38 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:9 +#: a20ab65e584c4daf9a9153008b713ee1 of +msgid "Relative folder path (folders only)." +msgstr "" + +#: 410039575afd4797a0eb1be9098ccda5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:11 +#: f0f56fb642874c3889fb0dacfed4f4ef of +msgid "File or folder name." +msgstr "" + +#: 214d3e0a88fa41d8a02af0a5d09c5ac6 72d2b6fdd324482eaea9803def1fe2da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:15 of +msgid "Transfer id shared by the pushed item." +msgstr "" + +#: 0aa1bd5b053544e98ae4ab3ae1add5f7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:17 +#: e7826b64fe174bf0a6f7faa4df49f491 of +msgid "Receiver-side destination directory." +msgstr "" + +#: 93e666db45e745619b90c744e1646b73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:20 of +msgid "Command line to hand to `send_message`." +msgstr "" + +#: 56d8327bb7414a5e85292f8f2ace9d42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:1 of +msgid "Push one forwarded file or folder item to a connected target." +msgstr "" + +#: 3098afa7f4ac43a184d081dda8faa6ba +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:3 of +msgid "" +"Public API for forward extensions: sends the line built by " +"`forward_target_command`, which the receiver attributes with " +"`parse_forward_originator`." +msgstr "" + +#: 9b9634061040430484cd2596f43a8feb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:22 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:25 +#: b8bb8c432f7649d5a35480fec0509a51 of +msgid "printed); True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:1 +#: b03f4edeca5f4cc19f6d695aac3690de of +msgid "Bind the server socket, then accept clients until `stop` runs." +msgstr "" + +#: 7e027ea659ab48e19ea346cb284fe144 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:3 of +msgid "" +"Blocks the calling thread. A console command thread is started when " +"``is_input_command_in_console`` is True, every accepted connection gets " +"its own `handle_client` thread, and a client beyond ``max_clients`` is " +"refused with a message. Socket errors and the end of the accept loop both" +" end in `stop`." +msgstr "" + +#: 291f5a8cc4f648ee9910e5cd45074675 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:1 of +msgid "Read console commands until the server stops." +msgstr "" + +#: 1ba2e428d432496688ef23f1c07562da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:3 of +msgid "" +"Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, " +"``/file_folder``, ``/multiple_file_multiple_client``, " +"``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward " +"commands are client-only and are refused here. Any other name goes to the" +" handlers registered with ``where_to_run=\"client\"``. Ctrl-C and EOF " +"stop the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:1 +#: eb90f49c77e64006a0c9ae861ff849b1 of +msgid "Stop the server and release everything it owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:3 +#: a059b1e71106487999c83c2e2042f509 of +msgid "" +"Closes the server socket and every client connection, flushes the message" +" and event stores, releases the allocated port range and clears " +"``running``. Safe to call more than once." +msgstr "" + +#: 32b06ea315b3404caa3a05c4aed0ce64 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:1 of +msgid "" +"TCP client: connect to a server, dispatch commands, send and receive " +"messages." +msgstr "" + +#: 2b30f5c9ecd24fc89cdcd58000ee1e99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:3 of +msgid "" +"Lines received from the server go through `receive_messages`: a line " +"starting with ``/`` is handled by `handle_server_command` (protocol " +"commands plus the handlers registered for the \"server\" side), any other" +" line is a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``. With " +"``is_input_command_in_console`` the console thread `interactive_mode` " +"sends typed lines to the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:12 +#: b9532c86786c43d5aea58b24157fefed of +msgid "Server address this client connects to." +msgstr "" + +#: 2cfa1e3e1674407ba1e2683e76fef65d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:18 of +msgid "Server port this client connects to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:24 +#: af04fdee2c95431a9b0c17f96e50c18c of +msgid "Local address the socket binds to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:30 +#: b1d4b5606e1c48b6a915815527542758 of +msgid "Local port, None when the OS chose one." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:32 +#: d926b4f3eb4b40a4acb21f780a4dc227 of +msgid "int | None" +msgstr "" + +#: 500cfd177cf5453d887f6904b73b7851 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:36 of +msgid "True while the connection is up." +msgstr "" + +#: 40f2a9e1b730422db3dd58bc2a6046e3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:1 of +msgid "Create the client and, unless extended, connect and start reading." +msgstr "" + +#: 0552aa36b7b74474af8f43b380dbee54 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:3 of +msgid "Server address to connect to; required before `connect` is called." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:6 +#: d246ff9aaf154bfa91b5c4c9d293afd5 of +msgid "Local address the socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:9 +#: e981b68ebbea46e9b556b09f4b5cd44a of +msgid "Server port. Defaults to 65432." +msgstr "" + +#: 766bf4c138234fe081bbd2668dcb2a5d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:11 of +msgid "Local port to bind; None lets the OS choose an ephemeral port." +msgstr "" + +#: 5d661c0f91f64ef39cacb1a5466d752c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:14 of +msgid "" +"Socket timeout in seconds for connect and receive. Must be None when " +"``is_wait_server`` is True." +msgstr "" + +#: 9478d02062284ecaa7838f931ab80780 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:17 of +msgid "Step between candidate ports in the allocation range. Defaults to 1." +msgstr "" + +#: 2e1695ae510b43758793257124fd23c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:22 of +msgid "Enter interactive mode after connecting. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:25 +#: e02da86957dd4f45bf84ede4e9c82cda of +msgid "Keep retrying while the server is not reachable. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:31 +#: e1fb7f67e1714a61a9e8fe500555c700 of +msgid "" +"When True, do not call `start_TCP_client`; the caller connects when " +"ready. Defaults to False." +msgstr "" + +#: 1634dbe84a95421a92a660f7c6fdd10e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:34 of +msgid "Negotiate the RSA-encrypted channel with the server. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:40 +#: cff66a7ac1ad410f96ed1ee03cb1cf53 of +msgid "" +"Buffer ceiling in MiB, kept for parity with the server class; the " +"client's forward path does not read it today. Defaults to 2048." +msgstr "" + +#: 0f06ff3b6db940da96ec09bee3dba526 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:45 of +msgid "If ``is_wait_server`` is True and ``timeout`` is not None." +msgstr "" + +#: 5dd677b9627d4b84b53dc7a6b2848b3f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:6 of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back as the response." +msgstr "" + +#: 0a57a7875bb94767904ff8d93fa77eb8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:10 of +msgid "" +"\"server\" for commands pushed by the server, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 876e8fe2a6d645829ba12ddbad5006df +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:1 of +msgid "Register ``listener(sender_id, message)`` for every inbound plain message." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:3 +#: d166a2cc8ea04bed82c5f6dd61dcdcf6 of +msgid "Mirrors the server-side contract; commands are not reported here." +msgstr "" + +#: 412834182c9442adaf0b85e24c200ccc +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:5 of +msgid "" +"``listener(sender_id, message)``; ``sender_id`` is the author's " +"``\"ip:port\"`` — the forwarding client for a message another client " +"forwarded here (``/send_msg_from`` envelope), or None for a direct push " +"from the server, which names no client author. It runs on the receive " +"thread, so it must not block, and exceptions raised inside it are " +"swallowed." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:1 +#: fc8a83e0b5f84273a8287b122c1a9a3e of +msgid "" +"Register ``listener(full_path, name, size, command)`` per saved inbound " +"file." +msgstr "" + +#: 34e3386853564aab9a824e96438fc3e5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:3 of +msgid "" +"Fired after a file pushed by the server (a direct send, or a forwarded " +"file/folder item) has been fully written to ``file_transfer_dir``." +msgstr "" + +#: 63f925709b83433e967610672b6fbc79 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:6 of +msgid "" +"``listener(full_path, name, size, command)``; ``command`` is the wire " +"command that triggered the transfer, so a listener can recognise protocol" +" pushes such as ``/crypto_pub_key``. It runs on the transfer thread, so " +"it must not block." +msgstr "" + +#: 0e91f82d8856443497c399f97643b757 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:1 of +msgid "Reserve this client's port range under the cross-process lock." +msgstr "" + +#: 6029a28126344e45b24394cb56b2f8f8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:3 of +msgid "No-op until the server assigns a range (see ``/client_alloc_port_range``)." +msgstr "" + +#: 32bed401c3574959ba5ed08fc9078401 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:1 of +msgid "Release this client's reserved port range." +msgstr "" + +#: 9f86b7aac4f44886be9cd87b56072b63 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:3 of +msgid "No-op unless a range was assigned (``is_hand_alloc_port`` True)." +msgstr "" + +#: 8154f96d9ec74bd588971a806d42f8c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the client port range for this process." +msgstr "" + +#: 69c2dd5d5d1d497c8bac1e62ab4fc30a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:1 +#: of +msgid "Report whether the client port range is reserved by some process." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_unlock:1 +#: c88f37ef2d5c47bda346e804508d6ec0 of +msgid "Remove the lock file that reserves the client port range." +msgstr "" + +#: 478443e187814a6ca7ad798277db023f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:1 of +msgid "Allocate the next free client port range and record it on disk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:3 +#: a8736540f4c3402fad9508e619d559d7 of +msgid "" +"``port`` is moved past the ranges already recorded by other clients on " +"this host, so each instance ends up with a range of its own." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:11 +#: bc44b1604e02467280b59cb8ad63d0af of +msgid "If the client port info file cannot be read or written." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_free_port:1 +#: e48dff576a074109a135db168b5dbe98 of +msgid "Drop this client's entry from the on-disk port range record." +msgstr "" + +#: 3aa62e8843c6495c862f43fc6dda9d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:1 of +msgid "Allocate a port, waiting until one is free." +msgstr "" + +#: 2f7752fa51d946b1a4a566b38f046e80 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:3 of +msgid "Allocated port, or 0 when no allocation range was assigned." +msgstr "" + +#: 503ab40fc32c4d548f65fa3272b2f4d2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:5 +#: b9fd549c95f24c2080d87cec92514db6 of +msgid "Allocated port; None when the upward range is exhausted; 0 when no" +msgstr "" + +#: 151a65f1c1984e4196c750108d0611cb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:6 of +msgid "allocation range was assigned." +msgstr "" + +#: 0c6201bb2d7e45d3a4916d7e50a21e10 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: 76c766a9c61f4075a762ccf55038a65e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:6 of +msgid "no allocation range was assigned." +msgstr "" + +#: 8d500513e8854ba4bb91c8905a9adb73 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:7 of +msgid "Local port to bind; None allocates one with `palloc`." +msgstr "" + +#: 840360623c8a4c2bbafe8fe7bbf9209c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:1 of +msgid "Connect to the server and start reading from it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:3 +#: df94288e660b48f6809358b3dc234ec9 of +msgid "" +"Binds ``client_port`` when one was configured, then retries while " +"``is_wait_server`` is True and the server is not reachable yet. Once the " +"socket is up the receive thread is started and the encryption mode is " +"negotiated, which closes the connection when the two sides disagree." +msgstr "" + +#: 22fccba0ab36499a9a7f4427abe5e9f4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:8 of +msgid "" +"True when the connection is established (and, if encryption is " +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 0270d0a2d42740a9889106339d9f2dbb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:10 of +msgid "True when the connection is established (and, if encryption is" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:11 +#: c194691cedfb48c6bfd97d9bdc6f2246 of +msgid "" +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 4dc38e3ef3b74b8e8c27378242a1925a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:1 of +msgid "Read from the server until the connection ends." +msgstr "" + +#: 431649a0d703476dadc6afa8adbe53d4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:3 of +msgid "" +"Runs on the receive thread: plain lines are reported to the message " +"listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes " +"are attributed to their sender first), other ``/`` lines go to " +"`handle_server_command`. Any end of the connection clears ``running`` and" +" releases the port range." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:1 +#: cf284b59f7f849039404ced20f8e12db of +msgid "Write one line to a socket, encrypting when the channel is up." +msgstr "" + +#: 40ba2ede2d3f4666b82107af129d24d0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:3 of +msgid "Target connection; the client passes ``self.client_socket``." +msgstr "" + +#: 2e663941eaf142cb96738add05f9ef4d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:10 of +msgid "" +"True when the payload was written; False when the client is not " +"running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: 38905ec7539d42e0839907c32dd8667f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:12 of +msgid "True when the payload was written; False when the client is not" +msgstr "" + +#: 3f7e2e6fedda476f996132c3f677f2f0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:13 of +msgid "running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:1 +#: a9220a130e694e0aba1d3b6320dff498 of +msgid "Send the payload of a console line to the server." +msgstr "" + +#: 2e208e83a89e4dac9793460a8808a042 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:3 of +msgid "" +"Console line such as ``/send_msg hello``; the first token (the command " +"name) is dropped and the second one is sent." +msgstr "" + +#: 39c8e70ca92b43b8ac687bc0aa7f4073 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:7 of +msgid "If the line has fewer than two tokens." +msgstr "" + +#: 8d47f30ae7b0451f8ecfe207ec835ff3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:1 of +msgid "Read up to ``msg_length`` bytes from a socket." +msgstr "" + +#: 10a39005f45c46728ef9a000eeaf9109 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:1 of +msgid "Dispatch one command line pushed by the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:3 +#: a7e781fde939475cb081204adac16ded of +msgid "" +"Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the" +" connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange " +"lines, and the transfer lines ``/file``, ``/file_folder``, " +"``/forward_upload``, ``/pause_trans``, ``/start_trans``, " +"``/forward_error``. Any other name goes to the handlers registered for " +"the \"server\" side via `register_command`; an unknown command is only " +"reported on the console." +msgstr "" + +#: 91942d88960c4793b5eab8c3654c1400 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:1 of +msgid "Forward plain messages to other connected clients through the server." +msgstr "" + +#: 75bb9147a7464a278e19af9d03b04ecb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:3 of +msgid "" +"The console command ``/forward_send_msg`` uses this; the client must be " +"connected. The server wraps each message in a ``/send_msg_from`` envelope" +" so the receiving client can attribute it back to this one." +msgstr "" + +#: 0f3c201936df45b687dc5532531b4a4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:7 of +msgid "Message texts to forward." +msgstr "" + +#: 277bbcb2143943b5b983badb00c3f4fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:9 of +msgid "Destination ``(ip, port)`` tuples." +msgstr "" + +#: 5ea8eea489fc43108a439d54fba31068 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:12 of +msgid "" +"True when the request was written to the server; False when the " +"client is not connected." +msgstr "" + +#: 2bc5d7e5dfa94eb29b49f12562f57c88 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:14 of +msgid "True when the request was written to the server; False when the" +msgstr "" + +#: 45d974ad946b408b91d41bcce65abcc8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:15 of +msgid "client is not connected." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:1 +#: c9401da7c37e40e5978c80e4876b11a0 of +msgid "Read console lines and act on them until the client stops." +msgstr "" + +#: 793bcfd02c014200b0ec47f8e15b4d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:3 of +msgid "" +"``/quit`` closes the connection; ``/send_msg``, ``/file``, " +"``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, " +"``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are " +"handled locally; any other name goes to the handlers registered with " +"``where_to_run=\"client\"``, and anything left is sent to the server as " +"it stands. Ctrl-C and EOF close the connection." +msgstr "" + +#: 6dbae803a56c442588b28af664b7a0e9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_file_console:1 of +msgid "" +"/forward_file ... ... [dest] (client " +"only)." +msgstr "" + +#: 05cad8b03e0747dc804ffbfda3122fe1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_folder_console:1 of +msgid "" +"/forward_folder ... ... [dest] " +"(client only)." +msgstr "" + +#: 6e9a9717a782419baecca63fd1f46baf +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:1 of +msgid "Close the connection and release everything the client owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:3 +#: ba6c5badecf6465db6036735870ed305 of +msgid "" +"Stops the receive loop, releases the port range, flushes the message and " +"event stores and closes the socket. Safe to call more than once." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:1 +#: c91a3245b93f46abbb49dbef7bcfeb1c of +msgid "Connect to the server and start the client loop." +msgstr "" + +#: 0c83552867484ea780fcff8d34e5c2d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:3 of +msgid "" +"Enters `interactive_mode` when ``is_input_command_in_console`` is True, " +"otherwise keeps the process alive while the connection is up. Exits the " +"process with status 1 when the connection cannot be established; Ctrl-C " +"and the end of the connection both run `close`." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po new file mode 100644 index 0000000..a00ea5e --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_udp.rst:2 +#: c777b065237a4eb1993c041388639a1d +msgid "PyFlow.network\\_api.connect\\_udp module" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.po new file mode 100644 index 0000000..c7d6604 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rst:2 9abd125493904de2bb64c9158a11243f +msgid "PyFlow.network\\_api package" +msgstr "" + +#: ../../api/PyFlow.network_api.rst:10 7012029060714904ba5d281c1d607be9 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po new file mode 100644 index 0000000..f8151c1 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po @@ -0,0 +1,232 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rsa_crypto.rst:2 +#: def9740b5a2e4aaeb2aeed8b4f02c0ec +msgid "PyFlow.network\\_api.rsa\\_crypto module" +msgstr "" + +#: 46d3090efafd4504870cf72a4bea22ab PyFlow.network_api.rsa_crypto:1 of +msgid "crypto_api (C/OpenSSL) RSA integration for PyFlow's TCP layer." +msgstr "" + +#: 5b1ff4a00b0f46258cda2e7440368de1 PyFlow.network_api.rsa_crypto:3 of +msgid "" +"A thin ctypes binding to the shared ``libcrypto_api`` plus the key " +"lifecycle required by the encrypted TCP channel:" +msgstr "" + +#: PyFlow.network_api.rsa_crypto:6 a5a0d6d600604683990a92555a49a3fe of +msgid "" +"Reuse an existing RSA keypair from ``~/.ssh`` (PEM private key) when one " +"is present and parseable, otherwise generate a fresh keypair into " +"``.Flow/pvt_key``. A caller-supplied keypair (``custom_keys``) is " +"honoured when both files parse and the pair matches." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:10 bb413dc908624589a3e57f8a2c702728 of +msgid "" +"Anti-MITM identity check (TOFU): every connection exchanges public keys " +"in plaintext. Each side records the peer in " +"``.Flow/pub_key/pub_key.json`` under the peer's ``(ip, port)`` with the " +"SHA-256 of its public key; a later connection from the same endpoint " +"presenting a different key is rejected, and a known key seen from a new " +"endpoint is re-registered under the new ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:16 aec1a7998276428881efcfac6fe4cebe of +msgid "" +"RSA-OAEP encrypt/decrypt with the ``_VALID`` plaintext signature so a " +"stale key (for example a rotated ``~/.ssh`` pair) is detected and the " +"peers re-exchange their public keys." +msgstr "" + +#: 60edbdead17c422782e688ab68f8a6d1 PyFlow.network_api.rsa_crypto:20 of +msgid "" +"The C library must be built first (``cmake -S . -B build && cmake --build" +" build``); see ``load_library`` for the search paths." +msgstr "" + +#: 67af7d5c5f41474fba915352e506a8b4 +#: PyFlow.network_api.rsa_crypto.CryptoLibraryError:1 of +msgid "Raised when the shared libcrypto_api cannot be loaded." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaKey:1 e620122ff0bc48dd985c4b6a0b3b41ce of +msgid "Owns an ``pf_rsa_key_t*`` handle; frees it on GC." +msgstr "" + +#: 1a7d8e9db5a3435486d716b13f2a2a56 +#: PyFlow.network_api.rsa_crypto.load_library:1 of +msgid "Locate and load the shared crypto_api library (cached)." +msgstr "" + +#: 670f4a719bf24c24b48ecb5d73b373ce +#: PyFlow.network_api.rsa_crypto.get_local_mac:1 of +msgid "Return a stable 48-bit machine identifier as colon-separated hex." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.get_local_mac:3 +#: f1306bb875d34b558f7418278fded413 of +msgid "" +"Uses ``uuid.getnode()`` (the real hardware MAC when one is available). " +"Server and client on the same host share this value; the ``_`` " +"prefix in the key file names keeps them apart." +msgstr "" + +#: 333763d798c54d52a9a56a3e4e3e2155 PyFlow.network_api.rsa_crypto.RsaCrypto:1 +#: of +msgid "Key lifecycle plus RSA-OAEP encrypt/decrypt for one role." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto:3 f52515121ac74575aaa36d45dd0341a2 +#: of +msgid "" +"``role`` is ``\"server\"`` or ``\"client\"`` and is used to name the " +"locally generated keypair (``pvt_key/_priv.pem``) and the peer key " +"cache (``pub_key/__.pem``). Peer identity is tracked" +" in ``pub_key/pub_key.json`` (TOFU, see ``verify_peer_pub``)." +msgstr "" + +#: 56fed5bcfe194b7f90a0c662ae36d6c0 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:1 of +msgid "Create the crypto wrapper for ``role`` (\"server\" or \"client\")." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:3 +#: ca4e27664ccf40ce935d991db43f7c69 of +msgid "" +"``custom_keys`` may be a ``[pub_key_path, pvt_key_path]`` pair to use a " +"user-supplied RSA keypair instead of the default lookup (``~/.ssh`` / " +"generated). The pair is validated on first use (paths exist, files parse," +" the keys match); an invalid pair is ignored and the default lookup is " +"used instead." +msgstr "" + +#: 7d89122ff0f54090b67209e3b08ae29c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:1 of +msgid "Load the RSA keypair (see module docstring) and cache handles." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:3 +#: d5e494e00be44891b33cb8e0ecfb8081 of +msgid "" +"Runs under ``_key_lock``: the private-key handle must never be replaced " +"(or freed on GC) while another thread is decrypting." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.reload_own_key:1 +#: f6f7d6ec8837464dad2dc647e6141a99 of +msgid "Re-read the private key (e.g. after a ~/.ssh rotation)." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:1 +#: e95d18da34d14376987a62f6cbbec778 of +msgid "" +"Path of the exchanged public key file for ``peer_role`` at ``(peer_ip, " +"peer_port)``." +msgstr "" + +#: 52dc6fb74f0c4175bfa2a2acc23cda0d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:4 of +msgid "" +"The IP is sanitized for the filesystem (``:`` -> ``_`` so IPv6 literals " +"are safe on every platform, including Windows)." +msgstr "" + +#: 731d92a1b64342dfbf7c467af0a4c00e +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:1 of +msgid "TOFU check-and-record for a peer public key." +msgstr "" + +#: 634283c14fb44424bf148a34d156fb71 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:3 of +msgid "" +"``peer_pem`` is the PEM text received on this connection, ``(peer_ip, " +"peer_port)`` the endpoint it came from. Returns ``(ok, reason)``:" +msgstr "" + +#: 6bb58c88f8454d7daca92b5a478ae788 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:7 of +msgid "" +"key already registered under any endpoint -> accept, and re-register it " +"under the current endpoint when it moved (IPs are dynamic and ports are " +"user-changeable);" +msgstr "" + +#: 74e2bcc89bae4fe5aecf99e6208e21d1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:10 of +msgid "" +"key unknown but the endpoint already holds a *different* key -> reject (a" +" trusted endpoint suddenly presenting a new key);" +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:12 +#: f856b875adc34f4a80c2b21cd506b143 of +msgid "" +"key and endpoint both unknown -> accept and record (first connection is " +"trusted, TOFU)." +msgstr "" + +#: 556fdf46d6c543e0ab06e2d4fabad7f1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:1 of +msgid "Move a freshly received public key file into the key cache." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:3 +#: a47e84f5d21949bf98e24834f18bed76 of +msgid "" +"Idempotent under concurrency: several transfers may deliver the same peer" +" key at once (multi-connection handshakes, several client processes " +"sharing one ``received_files/`` directory); if the source is already gone" +" because a concurrent store moved it, success is assumed when the " +"destination is in place." +msgstr "" + +#: 135944d28da54948862e515eb828072d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:1 of +msgid "Encrypt ``plaintext`` with the peer's public key file." +msgstr "" + +#: 4b4dded0ad904ce2bb87c9a43c8f87e5 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:3 of +msgid "" +"Returns the ASCII wire body (no trailing newline): each chunk is RSA-OAEP" +" encrypted and base64 encoded, chunks joined with ``|``. Raises if no " +"peer key is stored at ``peer_pem_path`` yet. The whole encryption runs " +"under ``_peer_pub_cache_lock`` so the peer handle cannot be freed mid-" +"encrypt (no-GIL safe)." +msgstr "" + +#: 8f1687f4ab984c029859fd1f9cfb968c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:1 of +msgid "Decrypt a wire body with our private key." +msgstr "" + +#: 8a1d1504bf4248fa9e1288776d19810b +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:3 of +msgid "" +"Returns ``(True, plaintext)`` on success, or ``(False, None)`` when the " +"key is stale/wrong or the ``_VALID`` signature is missing. Runs under " +"``_key_lock`` so the handle cannot be freed by a concurrent " +"``reload_own_key`` (no-GIL safe)." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.po new file mode 100644 index 0000000..a375167 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.rst:2 738ded08e11742acb4854652f885aa13 +msgid "PyFlow package" +msgstr "" + +#: ../../api/PyFlow.rst:10 8a3a28b0487c4bacb81f0afaa1b2902e +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.rst:19 28ca6a63167d49798f21d2796d6acf1e +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.po new file mode 100644 index 0000000..089c357 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.rst:2 e8518825f60e48f391b84d3fb415bb36 +msgid "PyFlow.transfer\\_web package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:10 8d8e622c8bdd4618b0e33d94e1000e42 +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:19 541ef21ed94743f2b9e11aadbde918b4 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po new file mode 100644 index 0000000..e369aad --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_client.rst:2 +#: 09f60d1b11ac4e92b480fa6284489970 +msgid "PyFlow.transfer\\_web.setup\\_client module" +msgstr "" + +#: 2f786e174934474a93c6498878a68612 PyFlow.transfer_web.setup_client:1 of +msgid "PyFlow TCP client web launcher." +msgstr "" + +#: 3e6b6a90f3e7484f8a0ca766f8305082 PyFlow.transfer_web.setup_client:3 of +msgid "" +"Starts a lightweight Flask backend on 127.0.0.1 and opens the connect UI " +"in the browser. The user enters the server address (an http/https domain" +" or a bare IP); the backend asks the server's web backend for the TCP " +"server address/port, starts the TCP client, and keeps the backend running" +" to relay the user's frontend actions." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po new file mode 100644 index 0000000..8322dfa --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_server.rst:2 +#: e6642bb55a204188b0abd0b07207713e +msgid "PyFlow.transfer\\_web.setup\\_server module" +msgstr "" + +#: 5966876a4564470ea674311ddb3ef63e PyFlow.transfer_web.setup_server:1 of +msgid "PyFlow TCP server web launcher." +msgstr "" + +#: 588fea0dc68a4a308f9a7cfb6fe5d819 PyFlow.transfer_web.setup_server:3 of +msgid "Checks ``transfer_web/.Flow_Web/setup_server.json``:" +msgstr "" + +#: PyFlow.transfer_web.setup_server:5 c3df4df524da40b18a58607efd9b5e4a of +msgid "" +"missing -> opens the server startup-configuration UI in the browser; the" +" UI saves the config (same shape as ``flow_setup``'s ``setup.json``) and " +"starts the TCP server class;" +msgstr "" + +#: 211850fce6d34d50ae152bce6d1aa3af PyFlow.transfer_web.setup_server:8 of +msgid "present -> starts the TCP server class directly from the saved config." +msgstr "" + +#: PyFlow.transfer_web.setup_server:10 cb510f0a77474eff8e8816fafc097343 of +msgid "" +"After the TCP server is up, the lightweight Flask backend serves the " +"status page and the client-facing API (``/api/server_info`` etc.) on the " +"server's address." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po new file mode 100644 index 0000000..92ef417 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:2 +#: 76090548cb6a4066a3558553adad502a +msgid "PyFlow.transfer\\_web.web\\_backend package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:10 +#: 0d60b135791b45019583e72be4a02afc +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po new file mode 100644 index 0000000..afeda2e --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po @@ -0,0 +1,119 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.server_backend.rst:2 +#: 4e16e69f9ae9491a95bfcab9c70cfe24 +msgid "PyFlow.transfer\\_web.web\\_backend.server\\_backend module" +msgstr "" + +#: 15eb6d4006b3429a8fa230a2b460b1c5 +#: PyFlow.transfer_web.web_backend.server_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP server for the web tool." +msgstr "" + +#: 3c413c9e37f94c7485dba6d4f96b9bf2 +#: PyFlow.transfer_web.web_backend.server_backend:3 of +msgid "Two modes, one process:" +msgstr "" + +#: 557b793ac8e441db86cf59cad5e85371 +#: PyFlow.transfer_web.web_backend.server_backend:5 of +msgid "" +"``config`` mode: serves the server startup-configuration UI. The UI " +"shows every ``TCP_Server_Base`` parameter with its default value; on " +"submit the config is written to ``.Flow_Web/setup_server.json`` (same " +"shape as ``flow_setup``'s ``setup.json``) and the TCP server class is " +"started." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:10 +#: cb922669795b42c6ad5587b527c4356a of +msgid "" +"``status`` mode: serves the minimal status page plus the same " +"sidebar/input UI as the client frontend (forwarding disabled; native " +"sends to connected clients allowed). Also exposes the HTTP API that " +"clients use to discover the TCP server address/port." +msgstr "" + +#: 0c6784517a194d65926c71b7ce7f4836 +#: PyFlow.transfer_web.web_backend.server_backend:15 of +msgid "" +"The backend monitors ``server.clients``: whenever a client connects or " +"disconnects it broadcasts the current instance list to every connected " +"client (``/web_clients_update``), and it re-checks the list every minute." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:20 +#: e902a63f173f4e8ba20923c4d8f4b083 of +msgid "" +"Inbound events (plain-text messages and file uploads arriving from " +"clients) are captured on the TCP server's receive threads through " +"``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` " +"APIs, queued here, and polled by the frontend via ``/api/events``." +msgstr "" + +#: 30582f045fae468cb63a543324edfea8 +#: PyFlow.transfer_web.web_backend.server_backend:25 of +msgid "" +"Authentication: anonymous visitors get a white landing page (the server " +"addresses plus a login button); the configuration and status pages need a" +" session. Accounts live in ``.Flow_Web/users.json``; the first run seeds" +" the ``admin``/``admin`` administrator, and the frontend warns on every " +"login until those default credentials are changed." +msgstr "" + +#: 5fbb587ae4814cd683d5940abf4af37b +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:1 of +msgid "Account store backing the server web login (``.Flow_Web/users.json``)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:3 +#: f16349131c524eba9d2fd07591592afb of +msgid "" +"Passwords are PBKDF2-SHA256 records with a per-user salt. A missing " +"store file seeds the default ``admin``/``admin`` administrator; a store " +"file that exists but cannot be read is *not* re-seeded, so a damaged file" +" can never silently restore the default account." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.authenticate:1 +#: cfd5d429421942b3b0576566b76575a8 of +#, python-brace-format +msgid "Return ``{\"username\", \"role\"}`` for valid credentials, else ``None``." +msgstr "" + +#: 56e3012561ed4ed4aa77ffea3a744f93 +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.change_credentials:1 +#: of +msgid "Rename ``username`` and set its password (self-service)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp:1 +#: ab4c37912c524489a26700248a06704c of +msgid "Flask app + TCP_Server_Base wrapper for the web tool." +msgstr "" + +#: 7e707a76895243cbb7e49d4a943df5f2 +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_server.json`` and start the TCP server." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po new file mode 100644 index 0000000..698d7d5 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.client_backend.rst:2 +#: a7e084a5059341aa9b7419059c4fa4b0 +msgid "PyFlow.transfer\\_web.web\\_front.client\\_backend module" +msgstr "" + +#: 22485674cb53445a86703fb118698523 +#: PyFlow.transfer_web.web_front.client_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP client for the web tool." +msgstr "" + +#: 31862d77d7184e55a1e729d7e72baed0 +#: PyFlow.transfer_web.web_front.client_backend:3 of +msgid "" +"The launcher (``setup_client.py``) starts this backend and opens the " +"connect UI in the browser. The user enters the server address (an " +"``http``/``https`` domain or a bare IP); the backend queries the server's" +" web backend ``/api/server_info`` for the TCP server address and port, " +"then starts the ``TCP_Client_Base`` instance. The backend stays up to " +"relay the user's frontend actions:" +msgstr "" + +#: 1a028258cda844ee945b9522d51afb9d +#: PyFlow.transfer_web.web_front.client_backend:10 of +msgid "messages/files/folders to the server use the native transfer methods;" +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:11 +#: e160c2a0daf9498cbe2041a8e964ff47 of +msgid "" +"messages to other clients use the native ``/forward_send_msg`` forwarding" +" (a client-only command relayed by the server);" +msgstr "" + +#: 16da18234f0a499893b711e7b555b42a +#: PyFlow.transfer_web.web_front.client_backend:13 of +msgid "" +"files/folders to other clients are forwarded through the built-in " +"``forward_extension_tcp`` extension." +msgstr "" + +#: 79dee862a03f4d0c9bc9403f8d461465 +#: PyFlow.transfer_web.web_front.client_backend:16 of +msgid "" +"The sidebar instance list is kept fresh by the server's " +"``/web_clients_update`` broadcasts; a reload button re-requests the list " +"via ``/web_sync_clients``." +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:20 +#: b952223fdafe4d52b8c34498aef1aeef of +msgid "" +"Inbound events (plain-text messages and files pushed by the server, " +"whether direct sends or client forwards) are captured on the TCP client's" +" receive threads through ``TCP_Client_Base``'s " +"``add_message_listener``/``add_file_listener`` APIs, queued here, and " +"polled by the frontend via ``/api/events``." +msgstr "" + +#: 0913e15917534119b775fb5c545c439d +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp:1 of +msgid "Flask app + TCP_Client_Base wrapper for the web tool." +msgstr "" + +#: 3e9a2f40d40b45869c4750ae3e542502 +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_client.json`` and start the TCP client." +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po new file mode 100644 index 0000000..75f1d43 --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.rst:2 +#: 24ab9a394e604e0ea77050304ef77edd +msgid "PyFlow.transfer\\_web.web\\_front package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_front.rst:10 +#: ae412dcb5fd34608b462f44c2a8b9a17 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ko/LC_MESSAGES/api/index.po b/docs/locale/ko/LC_MESSAGES/api/index.po new file mode 100644 index 0000000..0a8617c --- /dev/null +++ b/docs/locale/ko/LC_MESSAGES/api/index.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ko\n" +"Language-Team: ko \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/index.rst:2 8d57b43b04b14ab8a07c05fb89785684 +msgid "API Reference" +msgstr "" + +#: ../../api/index.rst:4 2e33b7c5195747ebb1a15eb5e3e9c026 +msgid "" +"The pages below are generated from the code by ``sphinx-apidoc`` (see the" +" first line of ``docs/reBuild.sh``): each one pulls its text from the " +"docstrings at build time, so nothing here is written by hand." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/File_Transfer/File_Transfer.po b/docs/locale/ru/LC_MESSAGES/File_Transfer/File_Transfer.po index c12bbee..facd032 100644 --- a/docs/locale/ru/LC_MESSAGES/File_Transfer/File_Transfer.po +++ b/docs/locale/ru/LC_MESSAGES/File_Transfer/File_Transfer.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 13:19+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: ru \n" @@ -773,11 +773,17 @@ msgid "Commands (client console only; rejected on the server console):" msgstr "Клиентская консоль (приемник — сервер):" #: ../../File_Transfer/File_Transfer.rst:413 14406316555743359a854540ab6937c8 -msgid "``/forward_file ... ...``" +#, fuzzy +msgid "" +"``/forward_file ... ... " +"[destination_file_path]``" msgstr "``/forward_file <файл1> <файл2> ... <адрес1> <адрес2> ...``" #: ../../File_Transfer/File_Transfer.rst:414 4dc576898177404da124649f7ce9ac28 -msgid "``/forward_folder ... ...``" +#, fuzzy +msgid "" +"``/forward_folder ... ... " +"[destination_file_path]``" msgstr "``/forward_folder <папка1> <папка2> ... <адрес1> <адрес2> ...``" #: ../../File_Transfer/File_Transfer.rst:416 220dfbf0a0d84559a8befb4eee500ba0 @@ -792,12 +798,28 @@ msgstr "" "недоступны (не подключены к серверу) или равны самому серверу, пропускаются," " а остальные цели по-прежнему обслуживаются." -#: ../../File_Transfer/File_Transfer.rst:424 f6697695e9ef4e3694fa9046e41578e0 +#: ../../File_Transfer/File_Transfer.rst:424 6137bf9406784c6f82d9f41560a52566 +msgid "" +"Like every transfer family, both commands accept an optional trailing " +"``destination_file_path`` that replaces the default save directory on every " +"receiving client: a forwarded file lands at ``/`` and" +" a forwarded folder keeps its structure under " +"``//...``. When the argument is omitted the " +"targets write to their default ``file_transfer_dir``." +msgstr "" +"Как и каждое семейство передачи, обе команды принимают необязательный " +"конечный ``destination_file_path``, который заменяет каталог сохранения по " +"умолчанию на каждом принимающем клиенте: переадресованный файл попадает на " +"``/``, а переадресованная папка сохраняет свою " +"структуру под ``//...``. Когда аргумент опущен, " +"конечные объекты записывают в файл по умолчанию ``file_transfer_dir``." + +#: ../../File_Transfer/File_Transfer.rst:435 f6697695e9ef4e3694fa9046e41578e0 msgid "The data path reuses the protocol's own transfer machinery:" msgstr "" "Путь к данным повторно использует собственный механизм передачи протокола:" -#: ../../File_Transfer/File_Transfer.rst:427 6ee037f0e4024d2988fcb2ccf3deef70 +#: ../../File_Transfer/File_Transfer.rst:438 6ee037f0e4024d2988fcb2ccf3deef70 msgid "" "The forwarding client streams the file with the standard file-transfer byte " "stream (metadata header + 64 KiB chunks) to a transfer socket on the server." @@ -806,7 +828,7 @@ msgstr "" "файлов (заголовок метаданных + фрагменты по 64 КиБ) в сокет передачи на " "сервере." -#: ../../File_Transfer/File_Transfer.rst:432 43c94e746a5c46748846d0dc18a4e0ff +#: ../../File_Transfer/File_Transfer.rst:443 43c94e746a5c46748846d0dc18a4e0ff msgid "" "The server acts as a pure relay: it reads the stream into per-target memory " "queues and writes each chunk to every target's transfer socket. The server " @@ -818,7 +840,7 @@ msgstr "" "цели. Сервер никогда не анализирует содержимое файла за пределами заголовка " "размера и никогда не записывает на диск." -#: ../../File_Transfer/File_Transfer.rst:439 9dec57916af34e109bee7ce1e0bae2d4 +#: ../../File_Transfer/File_Transfer.rst:450 9dec57916af34e109bee7ce1e0bae2d4 msgid "" "Every target client receives the stream with the ordinary receive path " "(``file_transfer_mode_recv``) and writes it to its own local disk, exactly " @@ -828,11 +850,11 @@ msgstr "" "(file_transfer_mode_recv) и записывает его на свой локальный диск, точно так" " же, как если бы сервер отправил файл напрямую." -#: ../../File_Transfer/File_Transfer.rst:445 a095abaf821e41a6a66adbb776bc35ef +#: ../../File_Transfer/File_Transfer.rst:456 a095abaf821e41a6a66adbb776bc35ef msgid "### Memory Bounding and Flow Control" msgstr "### Ограничение памяти и управление потоком" -#: ../../File_Transfer/File_Transfer.rst:447 244214dd675345f8a72b1088ad99fea0 +#: ../../File_Transfer/File_Transfer.rst:458 244214dd675345f8a72b1088ad99fea0 msgid "" "Because uploader, server and targets may have different bandwidths, data can" " pile up in the server's memory. Both ``TCP_Server_Base`` and " @@ -861,11 +883,11 @@ msgstr "" "``/pause_trans``/``/start_trans`` существуют на обеих сторонах, поэтому " "любая сторона может регулировать передачу при буферизации данных." -#: ../../File_Transfer/File_Transfer.rst:472 0aa14deaf6474437b421a65e21fce7d8 +#: ../../File_Transfer/File_Transfer.rst:483 0aa14deaf6474437b421a65e21fce7d8 msgid "Concurrency and Threading" msgstr "Параллелизм и многопоточность" -#: ../../File_Transfer/File_Transfer.rst:474 825c5e1a8b9b405c967ca49a1861e258 +#: ../../File_Transfer/File_Transfer.rst:485 825c5e1a8b9b405c967ca49a1861e258 msgid "" "Both the server and the client use multiple levels of concurrency control to" " ensure stability during file transfers." @@ -873,15 +895,15 @@ msgstr "" "И сервер, и клиент используют несколько уровней управления параллелизмом для" " обеспечения стабильности во время передачи файлов." -#: ../../File_Transfer/File_Transfer.rst:478 0d48b1245cd54597928cfc28d5b3c248 +#: ../../File_Transfer/File_Transfer.rst:489 0d48b1245cd54597928cfc28d5b3c248 msgid "### File Transfer Semaphore" msgstr "### Семафор передачи файлов" -#: ../../File_Transfer/File_Transfer.rst:480 e2674ee9e0584513bc53aa815603fd24 +#: ../../File_Transfer/File_Transfer.rst:491 e2674ee9e0584513bc53aa815603fd24 msgid "Client: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" msgstr "Клиент: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:482 1c0d27d13ad844159d29c6e662d51d09 +#: ../../File_Transfer/File_Transfer.rst:493 1c0d27d13ad844159d29c6e662d51d09 msgid "" "Server: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" @@ -889,7 +911,7 @@ msgstr "" "Сервер: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:485 5b0c8289b2c9460cb7305101226b66a7 +#: ../../File_Transfer/File_Transfer.rst:496 5b0c8289b2c9460cb7305101226b66a7 msgid "" "This semaphore limits the number of simultaneous file transfers (used " "primarily when sending folders or multiple files). Each transfer runs in its" @@ -900,11 +922,11 @@ msgstr "" "передача выполняется в своем собственном потоке, и семафор получается до " "запуска потока." -#: ../../File_Transfer/File_Transfer.rst:492 6d93783b298e4ce98b999ff890d4a5f4 +#: ../../File_Transfer/File_Transfer.rst:503 6d93783b298e4ce98b999ff890d4a5f4 msgid "### Threading Model" msgstr "### Модель потоков" -#: ../../File_Transfer/File_Transfer.rst:494 036a972600e645119d8a523fa08a52db +#: ../../File_Transfer/File_Transfer.rst:505 036a972600e645119d8a523fa08a52db msgid "" "Each file transfer runs in a dedicated daemon thread, created by the " "``_thread`` wrapper functions (e.g., " @@ -916,7 +938,7 @@ msgstr "" "``file_transfer_client_recv_client_start_thread``). Это предотвращает " "блокировку основного контура управления при медленной передаче." -#: ../../File_Transfer/File_Transfer.rst:500 a84aaf5f9fb64d92b97c32185a1e7cb5 +#: ../../File_Transfer/File_Transfer.rst:511 a84aaf5f9fb64d92b97c32185a1e7cb5 msgid "" "The thread that receives the transfer command (e.g., the server's " "``handle_command`` thread) does not wait for the transfer to complete; it " @@ -926,7 +948,7 @@ msgstr "" "сервера), не ждет завершения передачи; он возвращается сразу после создания " "рабочего потока." -#: ../../File_Transfer/File_Transfer.rst:505 669fb6da8e7444dd829a6e14295648b1 +#: ../../File_Transfer/File_Transfer.rst:516 669fb6da8e7444dd829a6e14295648b1 msgid "" "The low-level receive function (``file_transfer_mode_recv``) blocks while " "reading from the transfer socket, but because it runs in a dedicated thread," @@ -936,11 +958,11 @@ msgstr "" "чтении из сокета передачи, но поскольку она выполняется в выделенном потоке," " основное соединение остается отзывчивым." -#: ../../File_Transfer/File_Transfer.rst:511 e39f8ae06ae84a6b84de8a3008ca17cb +#: ../../File_Transfer/File_Transfer.rst:522 e39f8ae06ae84a6b84de8a3008ca17cb msgid "### Thread Pool for Custom Commands" msgstr "### Пул потоков для пользовательских команд" -#: ../../File_Transfer/File_Transfer.rst:513 3ad49853529d4563bacdfb4ca6eee115 +#: ../../File_Transfer/File_Transfer.rst:524 3ad49853529d4563bacdfb4ca6eee115 msgid "" "Both classes also provide a ``ThreadPoolExecutor`` " "(``self._custom_executor``) for custom command handlers. When a handler is " @@ -956,11 +978,11 @@ msgstr "" "``max_custom_workers``. Этот механизм **независим** от семафора передачи " "файлов и предназначен для обработки команд общего назначения." -#: ../../File_Transfer/File_Transfer.rst:528 1862d74da28a40219f82cfd7af89c126 +#: ../../File_Transfer/File_Transfer.rst:539 1862d74da28a40219f82cfd7af89c126 msgid "Port Allocation and Management" msgstr "Распределение портов и управление ими" -#: ../../File_Transfer/File_Transfer.rst:530 bd53ec8aa4ef498aa0815db16ed03140 +#: ../../File_Transfer/File_Transfer.rst:541 bd53ec8aa4ef498aa0815db16ed03140 msgid "" "File transfers require ephemeral ports for the secondary data connections. " "The ``palloc()`` and ``pfree()`` methods are used to obtain and release " @@ -970,7 +992,7 @@ msgstr "" "данным. Методы palloc() и pfree() используются для получения и освобождения " "этих портов. Доступны два режима:" -#: ../../File_Transfer/File_Transfer.rst:536 eeaa9379d7d941159ac34f30ff6bb5f9 +#: ../../File_Transfer/File_Transfer.rst:547 eeaa9379d7d941159ac34f30ff6bb5f9 msgid "" "**Automatic mode** (``is_hand_alloc_port=False``): ``palloc()`` returns " "``0``, and the operating system assigns a free port when the socket is " @@ -981,7 +1003,7 @@ msgstr "" "сокет привязан. Это рекомендуемый режим для большинства случаев " "использования." -#: ../../File_Transfer/File_Transfer.rst:541 80e08ac6614b4dcd88cd0e4d31a3da64 +#: ../../File_Transfer/File_Transfer.rst:552 80e08ac6614b4dcd88cd0e4d31a3da64 msgid "" "**Manual mode** (``is_hand_alloc_port=True``): Ports are drawn from a " "configurable range ``[self.min_port, self.max_port]`` with a step size " @@ -995,7 +1017,7 @@ msgstr "" "``/client_alloc_port_range``, и клиенты затем используют ту же логику " "ручного выделения." -#: ../../File_Transfer/File_Transfer.rst:551 29cfdab183eb4f9f83de41ef62a60785 +#: ../../File_Transfer/File_Transfer.rst:562 29cfdab183eb4f9f83de41ef62a60785 msgid "" "*Note: For more details about port allocation, please visit the Port " "Allocation API sections in :doc:`TCP_Server_APIs` and " @@ -1005,15 +1027,15 @@ msgstr "" " посетите разделы API распределения портов в документах " ":doc:`TCP_Server_APIs` и :doc:`TCP_Client_APIs`.*" -#: ../../File_Transfer/File_Transfer.rst:559 189736d8d1f8464b8d301f219700fda4 +#: ../../File_Transfer/File_Transfer.rst:570 189736d8d1f8464b8d301f219700fda4 msgid "Error Handling and Timeouts" msgstr "Обработка ошибок и тайм-ауты" -#: ../../File_Transfer/File_Transfer.rst:561 bcc373777989436f9df3657446d1a7f2 +#: ../../File_Transfer/File_Transfer.rst:572 bcc373777989436f9df3657446d1a7f2 msgid "### Timeout Values" msgstr "### Значения тайм-аута" -#: ../../File_Transfer/File_Transfer.rst:563 27d75d097a804ec28ebec5c67469c9c6 +#: ../../File_Transfer/File_Transfer.rst:574 27d75d097a804ec28ebec5c67469c9c6 msgid "" "**Start signal timeout**: 10 seconds. If the receiver does not send " "``server_start_file_transfer_sign`` within this time, the sender aborts." @@ -1022,7 +1044,7 @@ msgstr "" "``server_start_file_transfer_sign`` в течение этого времени, отправитель " "прерывает работу." -#: ../../File_Transfer/File_Transfer.rst:567 93e877c411eb4c96bd7861ff61f35e7a +#: ../../File_Transfer/File_Transfer.rst:578 93e877c411eb4c96bd7861ff61f35e7a msgid "" "**Port negotiation timeout**: 20 seconds. The initiator waits for the peer's" " ``/server_file_transfer_port`` response." @@ -1030,7 +1052,7 @@ msgstr "" "**Тайм-аут согласования порта**: 20 секунд. Инициатор ожидает ответа узла " "``/server_file_transfer_port``." -#: ../../File_Transfer/File_Transfer.rst:570 9e1d395bb59540a49393bebe6630e5eb +#: ../../File_Transfer/File_Transfer.rst:581 9e1d395bb59540a49393bebe6630e5eb msgid "" "**Completion acknowledgement timeout**: ``30 + (file_size // (100 * 1024 * " "1024)) * 10`` seconds. Larger files get proportionally more time." @@ -1039,11 +1061,11 @@ msgstr "" "1024)) * 10`` секунд. Файлам большего размера требуется пропорционально " "больше времени." -#: ../../File_Transfer/File_Transfer.rst:574 6b4e6135669b41178fdbb47ea5975381 +#: ../../File_Transfer/File_Transfer.rst:585 6b4e6135669b41178fdbb47ea5975381 msgid "### Error Signalling" msgstr "### Сигнализация об ошибках" -#: ../../File_Transfer/File_Transfer.rst:576 448fdc6ba3dd4d9eae925e7e241f8cb8 +#: ../../File_Transfer/File_Transfer.rst:587 448fdc6ba3dd4d9eae925e7e241f8cb8 msgid "" "Any error during the handshake or data transfer causes the failing side to " "send ``error_sign`` over the transfer socket." @@ -1051,7 +1073,7 @@ msgstr "" "Любая ошибка во время установления связи или передачи данных приводит к " "тому, что сбойная сторона отправляет ``error_sign`` через сокет передачи." -#: ../../File_Transfer/File_Transfer.rst:579 5b4c23a93e984ce0af2e43cf4752225b +#: ../../File_Transfer/File_Transfer.rst:590 5b4c23a93e984ce0af2e43cf4752225b msgid "" "The other side, upon receiving the error sign, closes the transfer socket " "and aborts the transfer." @@ -1059,7 +1081,7 @@ msgstr "" "Другая сторона, получив знак ошибки, закрывает сокет передачи и прерывает " "передачу." -#: ../../File_Transfer/File_Transfer.rst:582 b337a2726ded4d619c5e8026bef3f6ea +#: ../../File_Transfer/File_Transfer.rst:593 b337a2726ded4d619c5e8026bef3f6ea msgid "" "The main control connection remains unaffected; only the transfer socket is " "closed." @@ -1067,11 +1089,11 @@ msgstr "" "Главное соединение управления остается неизменным; закрыт только " "передаточный сокет." -#: ../../File_Transfer/File_Transfer.rst:586 68be52c8e5ce447a9c5ec51a661229cf +#: ../../File_Transfer/File_Transfer.rst:597 68be52c8e5ce447a9c5ec51a661229cf msgid "### Exception Handling" msgstr "### Обработка исключений" -#: ../../File_Transfer/File_Transfer.rst:588 036db9bc1858417ca589816028b83f80 +#: ../../File_Transfer/File_Transfer.rst:599 036db9bc1858417ca589816028b83f80 msgid "" "All socket operations are wrapped in try-except blocks. When an exception " "occurs (e.g., connection reset, file not found), the error is logged with " @@ -1084,11 +1106,11 @@ msgstr "" "прерывается. Если возможно, отправляется ``error_sign``, и сокет передачи " "закрывается." -#: ../../File_Transfer/File_Transfer.rst:600 fe9377db9cf04b9da0dbe4d07c730adf +#: ../../File_Transfer/File_Transfer.rst:611 fe9377db9cf04b9da0dbe4d07c730adf msgid "Related API Definitions" msgstr "Связанные определения API" -#: ../../File_Transfer/File_Transfer.rst:602 cc71cb9b1d6642a2acc89d45a49022cc +#: ../../File_Transfer/File_Transfer.rst:613 cc71cb9b1d6642a2acc89d45a49022cc msgid "" "This section lists all public file-transfer related methods in " "``TCP_Server_Base`` and ``TCP_Client_Base``. For a complete list of all " @@ -1098,11 +1120,11 @@ msgstr "" "файлов, в TCP_Server_Base и TCP_Client_Base. Полный список всех " "общедоступных API см. в таблицах в конце этого документа." -#: ../../File_Transfer/File_Transfer.rst:608 d6683e415f794c5bb693f8c24370e7f9 +#: ../../File_Transfer/File_Transfer.rst:619 d6683e415f794c5bb693f8c24370e7f9 msgid "### Server-Side File Transfer APIs" msgstr "### API-интерфейсы передачи файлов на стороне сервера" -#: ../../File_Transfer/File_Transfer.rst:618 9123c69197c34d93bb68d088897ebeca +#: ../../File_Transfer/File_Transfer.rst:629 9123c69197c34d93bb68d088897ebeca msgid "" "Initiates a server-to-client file transfer. ``message`` is the command " "string (e.g., ``/file /path/to/file.txt (127.0.0.1,54321)``). If " @@ -1114,11 +1136,11 @@ msgstr "" "указан ``file_folder_abspath`` (для передачи папок), он указывает абсолютный" " путь к родительской папке." -#: ../../File_Transfer/File_Transfer.rst:633 367dfd82c95c413d963a15152469fc44 +#: ../../File_Transfer/File_Transfer.rst:644 367dfd82c95c413d963a15152469fc44 msgid "Thread-safe version that starts a new thread for the transfer." msgstr "Потокобезопасная версия, запускающая новый поток для передачи." -#: ../../File_Transfer/File_Transfer.rst:642 d4aa76b7eb2c46c29adee0120a939b66 +#: ../../File_Transfer/File_Transfer.rst:653 d4aa76b7eb2c46c29adee0120a939b66 msgid "" "Sends an entire folder from server to client. ``message`` should be of the " "form ``/file_folder ``." @@ -1126,7 +1148,7 @@ msgstr "" "Отправляет всю папку с сервера клиенту. ``сообщение`` должно иметь форму " "``/папка_файла <путь_к папке> <адрес_клиента>``." -#: ../../File_Transfer/File_Transfer.rst:652 fcb785ff144746fab81e95ec2ab056e1 +#: ../../File_Transfer/File_Transfer.rst:663 fcb785ff144746fab81e95ec2ab056e1 msgid "" "Sends multiple files to multiple clients. The message format is " "``/multiple_file_multiple_client ... " @@ -1136,7 +1158,7 @@ msgstr "" "``/multiple_file_multiple_client ... " " ...``. Файлы должны появляться перед клиентами." -#: ../../File_Transfer/File_Transfer.rst:664 9c6f024528f344e399b62023c7f8c858 +#: ../../File_Transfer/File_Transfer.rst:675 9c6f024528f344e399b62023c7f8c858 msgid "" "Sends different file lists to different clients. The message alternates " "between groups: a list of files, then a list of client addresses, then the " @@ -1148,7 +1170,7 @@ msgstr "" "список файлов и т. д. Пример: ``/diff_multiple_file_diff_multiple_client " "a.txt b.txt (ip1,port1) (ip2,port2) c.txt (ip3,port3)``" -#: ../../File_Transfer/File_Transfer.rst:682 f8a880287b7b49d4bdc2239ecf4a0577 +#: ../../File_Transfer/File_Transfer.rst:693 f8a880287b7b49d4bdc2239ecf4a0577 msgid "" "Receives a file from a client. Called internally when the server receives a " "``/file`` command from a client." @@ -1156,7 +1178,7 @@ msgstr "" "Получает файл от клиента. Вызывается внутренне, когда сервер получает " "команду ``/file`` от клиента." -#: ../../File_Transfer/File_Transfer.rst:698 97bfaa9c24ae47c39328707b8f17a91a +#: ../../File_Transfer/File_Transfer.rst:709 97bfaa9c24ae47c39328707b8f17a91a msgid "" "Low-level receive function that performs the handshake and writes the " "incoming file to disk." @@ -1164,7 +1186,7 @@ msgstr "" "Функция приема низкого уровня, которая выполняет подтверждение связи и " "записывает входящий файл на диск." -#: ../../File_Transfer/File_Transfer.rst:711 db6a97ca42ba43e59d5c20695039d4ee +#: ../../File_Transfer/File_Transfer.rst:722 db6a97ca42ba43e59d5c20695039d4ee msgid "" "Low-level send function that connects to the receiver and transmits the " "file." @@ -1172,11 +1194,11 @@ msgstr "" "Функция отправки низкого уровня, которая подключается к получателю и " "передает файл." -#: ../../File_Transfer/File_Transfer.rst:713 3e7b689215e840bebd368b4d29104ebc +#: ../../File_Transfer/File_Transfer.rst:724 3e7b689215e840bebd368b4d29104ebc msgid "### Client-Side File Transfer APIs" msgstr "### API передачи файлов на стороне клиента" -#: ../../File_Transfer/File_Transfer.rst:723 1e97e492d6504579a9a265eb1242395e +#: ../../File_Transfer/File_Transfer.rst:734 1e97e492d6504579a9a265eb1242395e msgid "" "Initiates a client-to-server file transfer. ``message`` is the user command " "(e.g., ``/file mydoc.txt``). Used internally by the interactive console." @@ -1185,50 +1207,50 @@ msgstr "" "пользователя (например, ``/file mydoc.txt``). Используется внутри " "интерактивной консоли." -#: ../../File_Transfer/File_Transfer.rst:735 -#: ../../File_Transfer/File_Transfer.rst:786 0cd2695763114a0b831df0bfa80a3d56 +#: ../../File_Transfer/File_Transfer.rst:746 +#: ../../File_Transfer/File_Transfer.rst:797 0cd2695763114a0b831df0bfa80a3d56 msgid "Thread-safe version." msgstr "Потокобезопасная версия." -#: ../../File_Transfer/File_Transfer.rst:744 261399ca508d463eafa7f03a00bfc658 +#: ../../File_Transfer/File_Transfer.rst:755 261399ca508d463eafa7f03a00bfc658 msgid "Sends a folder from client to server." msgstr "Отправляет папку с клиента на сервер." -#: ../../File_Transfer/File_Transfer.rst:753 ef5e3b92b11c4530960c1c344a51c73b +#: ../../File_Transfer/File_Transfer.rst:764 ef5e3b92b11c4530960c1c344a51c73b msgid "Sends multiple files from client to server." msgstr "Отправляет несколько файлов с клиента на сервер." -#: ../../File_Transfer/File_Transfer.rst:762 f2c90ee949d7484480cbb2cd5310bf26 +#: ../../File_Transfer/File_Transfer.rst:773 f2c90ee949d7484480cbb2cd5310bf26 msgid "Sends multiple folders from client to server." msgstr "Отправляет несколько папок с клиента на сервер." -#: ../../File_Transfer/File_Transfer.rst:775 0b13d26a40174243a15698b4bfcbb69f +#: ../../File_Transfer/File_Transfer.rst:786 0b13d26a40174243a15698b4bfcbb69f msgid "" "Receives a file from the server (called when the server initiates a " "transfer)." msgstr "" "Получает файл с сервера (вызывается, когда сервер инициирует передачу)." -#: ../../File_Transfer/File_Transfer.rst:797 3b78a4355d7f4ee2bbbe6bf934a962c0 +#: ../../File_Transfer/File_Transfer.rst:808 3b78a4355d7f4ee2bbbe6bf934a962c0 msgid "Receives a folder from the server." msgstr "Получает папку с сервера." -#: ../../File_Transfer/File_Transfer.rst:812 3d044e0754b94d1289b491502ce83610 +#: ../../File_Transfer/File_Transfer.rst:823 3d044e0754b94d1289b491502ce83610 msgid "Low-level receive function on the client side." msgstr "Низкоуровневая функция приема на стороне клиента." -#: ../../File_Transfer/File_Transfer.rst:824 7311023a7fa644ed9b57a2873cd3bca8 +#: ../../File_Transfer/File_Transfer.rst:835 7311023a7fa644ed9b57a2873cd3bca8 msgid "" "Low‑level send function on the client side (identical to server's version)." msgstr "" "Функция отправки низкого уровня на стороне клиента (идентична серверной " "версии)." -#: ../../File_Transfer/File_Transfer.rst:829 7e74e9a09a8a4cf2a8a372a50b1ee51b +#: ../../File_Transfer/File_Transfer.rst:840 7e74e9a09a8a4cf2a8a372a50b1ee51b msgid "Public API Summary" msgstr "Сводка общедоступного API" -#: ../../File_Transfer/File_Transfer.rst:831 87aaf0a5d3b44f41a419d97b6567f6d0 +#: ../../File_Transfer/File_Transfer.rst:842 87aaf0a5d3b44f41a419d97b6567f6d0 msgid "" "All public APIs (including non-file-transfer methods) are listed below for " "reference." @@ -1236,33 +1258,33 @@ msgstr "" "Все общедоступные API (включая методы, не связанные с передачей файлов) " "перечислены ниже для справки." -#: ../../File_Transfer/File_Transfer.rst:835 a195ee393f2a442c810e59811a6ae126 +#: ../../File_Transfer/File_Transfer.rst:846 a195ee393f2a442c810e59811a6ae126 msgid "### TCP_Server_Base Public APIs" msgstr "### Общедоступные API TCP_Server_Base" -#: ../../File_Transfer/File_Transfer.rst:837 0408d74a9140472e9a774143a60e5749 +#: ../../File_Transfer/File_Transfer.rst:848 0408d74a9140472e9a774143a60e5749 msgid "``file_transfer_server_recv_client_start``" msgstr "``file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:838 24a33cb212844e04a8a271ada32412f0 +#: ../../File_Transfer/File_Transfer.rst:849 24a33cb212844e04a8a271ada32412f0 msgid "``file_transfer_server_recv_client_start_thread``" msgstr "``file_transfer_server_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:839 7e286d1ece6940868bea6b937486e617 +#: ../../File_Transfer/File_Transfer.rst:850 7e286d1ece6940868bea6b937486e617 msgid "``folder_file_transfer_server_recv_client_start``" msgstr "``folder_file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:840 37fc5bd346c84415bb15138a42508fbe +#: ../../File_Transfer/File_Transfer.rst:851 37fc5bd346c84415bb15138a42508fbe msgid "``multiple_file_multiple_client_transfer_server_recv_client_start``" msgstr "``multiple_file_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:841 38a2b16e3ab648688cf7e8d1cae8be72 +#: ../../File_Transfer/File_Transfer.rst:852 38a2b16e3ab648688cf7e8d1cae8be72 msgid "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" msgstr "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:843 11476e0eaa3b4a4286612812e4e2c004 +#: ../../File_Transfer/File_Transfer.rst:854 11476e0eaa3b4a4286612812e4e2c004 msgid "" "(The low-level helpers ``file_transfer_server_recv_server_start``, " "``file_transfer_mode_recv``, and ``file_transfer_mode`` are not considered " @@ -1272,53 +1294,53 @@ msgstr "" "``file_transfer_mode_recv`` и ``file_transfer_mode`` не считаются " "общедоступными, но для полноты документированы.)" -#: ../../File_Transfer/File_Transfer.rst:849 907e861cebe648fbacb799bae8bb15e0 +#: ../../File_Transfer/File_Transfer.rst:860 907e861cebe648fbacb799bae8bb15e0 msgid "### TCP_Client_Base Public APIs" msgstr "### Публичные API TCP_Client_Base" -#: ../../File_Transfer/File_Transfer.rst:851 88bd80083f754caeb026f9ce1b8c6b55 +#: ../../File_Transfer/File_Transfer.rst:862 88bd80083f754caeb026f9ce1b8c6b55 msgid "``file_transfer_client_recv_client_start``" msgstr "``file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:852 3b21ff73da694465906482412b4fb4e3 +#: ../../File_Transfer/File_Transfer.rst:863 3b21ff73da694465906482412b4fb4e3 msgid "``file_transfer_client_recv_client_start_thread``" msgstr "``file_transfer_client_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:853 c8af43896a2443a5b14bd29863731c2c +#: ../../File_Transfer/File_Transfer.rst:864 c8af43896a2443a5b14bd29863731c2c msgid "``folder_file_transfer_client_recv_client_start``" msgstr "``folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:854 7a3d3e4a52334b169a62d3b30d7a3190 +#: ../../File_Transfer/File_Transfer.rst:865 7a3d3e4a52334b169a62d3b30d7a3190 msgid "``multiple_file_transfer_client_recv_client_start``" msgstr "``multiple_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:855 19e18754c4674842ad5116f709588e03 +#: ../../File_Transfer/File_Transfer.rst:866 19e18754c4674842ad5116f709588e03 msgid "``multiple_folder_file_transfer_client_recv_client_start``" msgstr "``multiple_folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:856 4cf80f9f34e045c59a680d0450c9103a +#: ../../File_Transfer/File_Transfer.rst:867 4cf80f9f34e045c59a680d0450c9103a msgid "``file_transfer_client_recv_server_start``" msgstr "``file_transfer_client_recv_server_start``" -#: ../../File_Transfer/File_Transfer.rst:857 c09f5fcba03f461f89238dd31abf6e88 +#: ../../File_Transfer/File_Transfer.rst:868 c09f5fcba03f461f89238dd31abf6e88 msgid "``file_transfer_client_recv_server_start_thread``" msgstr "``file_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:858 ad39bc6b71f048b09bca3b895e9d32f8 +#: ../../File_Transfer/File_Transfer.rst:869 ad39bc6b71f048b09bca3b895e9d32f8 msgid "``file_folder_transfer_client_recv_server_start_thread``" msgstr "``file_folder_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:860 99140d0be72649199911a57a26e2f2cf +#: ../../File_Transfer/File_Transfer.rst:871 99140d0be72649199911a57a26e2f2cf msgid "(The low-level helpers are documented but not part of the public API.)" msgstr "" "(Помощники низкого уровня документированы, но не являются частью " "общедоступного API.)" -#: ../../File_Transfer/File_Transfer.rst:864 60355543ac904504af8529431ce2c1fa +#: ../../File_Transfer/File_Transfer.rst:875 60355543ac904504af8529431ce2c1fa msgid "See Also" msgstr "См. также" -#: ../../File_Transfer/File_Transfer.rst:866 7a5902ad3de64bf79d54d7f2f83ecbfb +#: ../../File_Transfer/File_Transfer.rst:877 7a5902ad3de64bf79d54d7f2f83ecbfb msgid "" "For more information about the TCP server and client base classes, please " "refer to:" @@ -1326,15 +1348,15 @@ msgstr "" "Дополнительные сведения о базовых классах TCP-сервера и клиента см. по " "адресу:" -#: ../../File_Transfer/File_Transfer.rst:870 f10c29f467b849e8b4254998b44f99ba +#: ../../File_Transfer/File_Transfer.rst:881 f10c29f467b849e8b4254998b44f99ba msgid ":doc:`../Network_APIs/TCP_Server_APIs`" msgstr ":doc:`../Network_APIs/TCP_Server_APIs`" -#: ../../File_Transfer/File_Transfer.rst:871 335ba244d28342449db065c252d7e14c +#: ../../File_Transfer/File_Transfer.rst:882 335ba244d28342449db065c252d7e14c msgid ":doc:`../Network_APIs/TCP_Client_APIs`" msgstr ":doc:`../Network_APIs/TCP_Client_APIs`" -#: ../../File_Transfer/File_Transfer.rst:873 92d0227c356447a098cba072d5b43c98 +#: ../../File_Transfer/File_Transfer.rst:884 92d0227c356447a098cba072d5b43c98 msgid "" "For details on port allocation, see the Port Allocation API sections in " "those documents." diff --git a/docs/locale/ru/LC_MESSAGES/Instance_Setup/Instance_Setup.po b/docs/locale/ru/LC_MESSAGES/Instance_Setup/Instance_Setup.po index 77c7a98..80e75fd 100644 --- a/docs/locale/ru/LC_MESSAGES/Instance_Setup/Instance_Setup.po +++ b/docs/locale/ru/LC_MESSAGES/Instance_Setup/Instance_Setup.po @@ -8,20 +8,20 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-30 23:45+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.18.0\n" -#: ../../Instance_Setup/Instance_Setup.rst:3 707444a8cdb246fd81a189c038518c82 +#: ../../Instance_Setup/Instance_Setup.rst:3 552b0fbec3774a638b0029ce5fd72949 msgid "Flow Setup Launcher" msgstr "Панель запуска настройки потока" -#: ../../Instance_Setup/Instance_Setup.rst:5 a611d1bbc1bc4b12a9db0997909626bc +#: ../../Instance_Setup/Instance_Setup.rst:5 31952c3324264ea28368c7725796ff63 msgid "" "The ``flow_setup.py`` script is a launcher for the TCP server/client " "framework defined in ``connect_tcp.py``. It allows you to quickly spawn a " @@ -36,7 +36,7 @@ msgstr "" "экземпляр запускается в отдельном окне терминала (или в фоновом процессе в " "автономных системах)." -#: ../../Instance_Setup/Instance_Setup.rst:12 bc0bbe4590e04437827678fe85f482fb +#: ../../Instance_Setup/Instance_Setup.rst:12 4f99dd7362fa4dcb8f83de37f327dbef msgid "" "**Note:** This launcher supports only **one server** and **one client** " "instance at a time. Adding a new server or client configuration will " @@ -46,22 +46,22 @@ msgstr "" "**один сервер** и **один клиент**. Добавление новой конфигурации сервера или" " клиента полностью перезапишет любую предыдущую конфигурацию того же типа." -#: ../../Instance_Setup/Instance_Setup.rst:18 146d87f809054469be52a0d4174907fc +#: ../../Instance_Setup/Instance_Setup.rst:18 c8f9652279de42bb983523a15048596e msgid "Features" msgstr "Функции" -#: ../../Instance_Setup/Instance_Setup.rst:20 ea5ca94177b349b3852287e9327f792e +#: ../../Instance_Setup/Instance_Setup.rst:20 fba5b9ea870c44fabf69022a45cdaebc msgid "" "**Interactive mode** – step‑by‑step creation of a server or client instance." msgstr "" "**Интерактивный режим** – пошаговое создание экземпляра сервера или клиента." -#: ../../Instance_Setup/Instance_Setup.rst:22 bb06f53ddbea4dd09b99e288569f858d +#: ../../Instance_Setup/Instance_Setup.rst:22 df9cbf9e1d024f9386a4b6278c82249e msgid "**Command‑line mode** – launch with all parameters in one command." msgstr "" "**Режим командной строки** — запуск со всеми параметрами одной командой." -#: ../../Instance_Setup/Instance_Setup.rst:24 11bd4b3ce97743e0857c109ae9bc2286 +#: ../../Instance_Setup/Instance_Setup.rst:24 5f7cba6df26144e9840d140f94065b33 msgid "" "**Persistent configuration** – stores the latest instance definitions in " "``setup.json`` (same directory as the script). Each type (server/client) " @@ -72,7 +72,7 @@ msgstr "" "(сервер/клиент) содержит **только одну** конфигурацию, которая " "перезаписывается при каждом обновлении." -#: ../../Instance_Setup/Instance_Setup.rst:29 7b3f201c93c349719f4128f05cc194bf +#: ../../Instance_Setup/Instance_Setup.rst:29 be9bc008af594306be21e731d6aefee3 msgid "" "**Cross‑platform** – supports Windows (cmd), Linux (gnome‑terminal, xterm, " "or background), and macOS (Terminal.app)." @@ -80,7 +80,7 @@ msgstr "" "**Кроссплатформенность** — поддерживает Windows (cmd), Linux (gnome-" "terminal, xterm или фоновый режим) и macOS (Terminal.app)." -#: ../../Instance_Setup/Instance_Setup.rst:32 89b17f1fa60241a288381f74072aa273 +#: ../../Instance_Setup/Instance_Setup.rst:32 57005cb0d04f47b2a8f10ad0bc9c7b43 msgid "" "**Complete parameter support** – all parameters accepted by " "``TCP_Server_Base`` and ``TCP_Client_Base`` can be stored in ``setup.json`` " @@ -90,35 +90,35 @@ msgstr "" "``TCP_Server_Base`` и ``TCP_Client_Base``, могут быть сохранены в " "``setup.json`` для точной настройки." -#: ../../Instance_Setup/Instance_Setup.rst:37 ca859ffa1d4140cab68092170525c59f +#: ../../Instance_Setup/Instance_Setup.rst:37 2ff27919a6f64e8fb610158de5ffdf69 msgid "Usage" msgstr "Использование" -#: ../../Instance_Setup/Instance_Setup.rst:40 1935db82ef7f4dbeb08d386a49aba876 +#: ../../Instance_Setup/Instance_Setup.rst:40 dddde4f2d7084bc48efa1296cfd3f22c msgid "Interactive Mode" msgstr "Интерактивный режим" -#: ../../Instance_Setup/Instance_Setup.rst:42 86f8dafb1c9d4adaa4666b1dea52af23 +#: ../../Instance_Setup/Instance_Setup.rst:42 f060e64959674f52b223bbc4b01d568c msgid "Run the script without any arguments:" msgstr "Запустите скрипт без аргументов:" -#: ../../Instance_Setup/Instance_Setup.rst:48 d516fe21071e448da15df91f07353fc8 +#: ../../Instance_Setup/Instance_Setup.rst:48 dd76918a5ffd44838c1b0c44595ae55b msgid "The script will ask you to:" msgstr "Скрипт попросит вас:" -#: ../../Instance_Setup/Instance_Setup.rst:50 41684f03f6d7460f87fed8f4bef8f1e5 +#: ../../Instance_Setup/Instance_Setup.rst:50 80847dcc3e404a779b2e693cf8de50d5 msgid "Choose the type (0 for Server, 1 for Client)." msgstr "Выберите тип (0 для сервера, 1 для клиента)." -#: ../../Instance_Setup/Instance_Setup.rst:51 750440862d7545e9bc4f137f36983fd6 +#: ../../Instance_Setup/Instance_Setup.rst:51 cebc8ba62d1a48a69d8019c3862dc18b msgid "Enter the bind address and port (``host:port``)." msgstr "Введите адрес привязки и порт («хост:порт»)." -#: ../../Instance_Setup/Instance_Setup.rst:52 a938572ed1dd43f7988e382ec9fea306 +#: ../../Instance_Setup/Instance_Setup.rst:52 4311b66e1b974b4596f2693908ba3109 msgid "If Client, also enter the server address and port to connect to." msgstr "Если Клиент, также введите адрес сервера и порт для подключения." -#: ../../Instance_Setup/Instance_Setup.rst:53 3f30b7f3c46e42c19eae6344b424e1f7 +#: ../../Instance_Setup/Instance_Setup.rst:53 65c194c1b655417c921c1e03daf7a17c msgid "" "Decide whether to add another instance (if you add the same type again, the " "previous configuration of that type is overwritten)." @@ -126,7 +126,7 @@ msgstr "" "Решите, добавлять ли еще один экземпляр (если вы снова добавите тот же тип, " "предыдущая конфигурация этого типа будет перезаписана)." -#: ../../Instance_Setup/Instance_Setup.rst:55 e8b6550fe5eb426f8478c1b3fef81160 +#: ../../Instance_Setup/Instance_Setup.rst:55 6bb60d44328244d4ab20f3ae5ba2b827 msgid "" "If ``setup.json`` already exists, you will be prompted to either reuse the " "existing configuration (launch the stored instances) or overwrite it with " @@ -136,7 +136,7 @@ msgstr "" "использовать существующую конфигурацию (запустить сохраненные экземпляры), " "либо перезаписать ее новыми определениями." -#: ../../Instance_Setup/Instance_Setup.rst:60 b222bf87298747c6b526d72e1459ad83 +#: ../../Instance_Setup/Instance_Setup.rst:60 9aeb2efa913341a89212e86cb7419e5a msgid "" "**Important:** When you choose to overwrite, the old server/client " "configuration is **completely replaced** by the new one. There is no " @@ -145,23 +145,70 @@ msgstr "" "**Важно!** При выборе перезаписи старая конфигурация сервера/клиента " "**полностью заменяется** новой. Никакого слияния нет." -#: ../../Instance_Setup/Instance_Setup.rst:65 ad560cd1d404495583c74b5929235453 +#: ../../Instance_Setup/Instance_Setup.rst:65 b5cd8be69a40420a89cf7ad69631cd45 msgid "Command‑line Mode" msgstr "Режим командной строки" -#: ../../Instance_Setup/Instance_Setup.rst:67 50a9125f52a54c9b82c66cf616e06d71 +#: ../../Instance_Setup/Instance_Setup.rst:67 d798ec606de2454da5f10e9d0a7e677c msgid "Use the following options:" msgstr "Используйте следующие параметры:" -#: ../../Instance_Setup/Instance_Setup.rst:82 f7f3b3263fdf457c908ea18b74dccea9 +#: ../../Instance_Setup/Instance_Setup.rst:70 4d5c1aebbede473c8598cbc45f8deb20 +msgid "Option" +msgstr "Вариант" + +#: ../../Instance_Setup/Instance_Setup.rst:70 a50ba126b168482997fae00497252fd4 +msgid "Description" +msgstr "Описание" + +#: ../../Instance_Setup/Instance_Setup.rst:72 7755904907ee46f6a34144360ed876f7 +#, python-brace-format +msgid "``--type {0,1}``" +msgstr "``--type {0,1}``" + +#: ../../Instance_Setup/Instance_Setup.rst:72 e0533cce8d9f4586aa838073b1ed3e2a +msgid "**Required.** 0 = Server, 1 = Client." +msgstr "**Обязательно.** 0 = Сервер, 1 = Клиент." + +#: ../../Instance_Setup/Instance_Setup.rst:74 7cc185b424ee489282d9d85571d54fae +msgid "``--setup_addr_port``" +msgstr "``--setup_addr_port``" + +#: ../../Instance_Setup/Instance_Setup.rst:74 37df8730dce9408a87ba48b775eb2b0a +#, fuzzy +msgid "**Required.** Bind address and port (e.g. ``127.0.0.1:8000``)." +msgstr "Введите адрес привязки и порт («хост:порт»)." + +#: ../../Instance_Setup/Instance_Setup.rst:77 388a3dd952ac47a78598792ef7d587bc +msgid "``--connect_addr_port``" +msgstr "``--connect_ADDR_Port``" + +#: ../../Instance_Setup/Instance_Setup.rst:77 b4f319ec304e4a26a09ac62b90db0a07 +#, fuzzy +msgid "Required for Client only. Server address and port to connect to." +msgstr "Если Клиент, также введите адрес сервера и порт для подключения." + +#: ../../Instance_Setup/Instance_Setup.rst:80 7bb925846a36488d945c3442889d83c0 +msgid "``--setup_num``" +msgstr "``--setup_num``" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7f3e0b1925924583acf7acb25ec6e183 +msgid "" +"*Ignored.* The script always launches a single instance. This flag is " +"accepted for compatibility but has no effect." +msgstr "" +"*Игнорируется.* Скрипт всегда запускает один экземпляр. Этот флаг принят для" +" совместимости, но не имеет эффекта." + +#: ../../Instance_Setup/Instance_Setup.rst:86 99952fa68b694654b82a309a26419152 msgid "Examples" msgstr "Примеры" -#: ../../Instance_Setup/Instance_Setup.rst:84 13847166f8d54c9db943e1e55cf66c55 +#: ../../Instance_Setup/Instance_Setup.rst:88 9c77e76f40a940e99e754d27e3bc1b05 msgid "**Launch a single server** on ``127.0.0.1:8000``:" msgstr "**Запустите один сервер** на ``127.0.0.1:8000``:" -#: ../../Instance_Setup/Instance_Setup.rst:90 aa92290b83324661a23d2343f7ca56fb +#: ../../Instance_Setup/Instance_Setup.rst:94 37df8730dce9408a87ba48b775eb2b0a msgid "" "**Launch a client** bound to port ``9000``, connecting to a server at " "``127.0.0.1:8000``:" @@ -169,19 +216,20 @@ msgstr "" "**Запустите клиент**, привязанный к порту ``9000`` и подключающийся к " "серверу по адресу ``127.0.0.1:8000``:" -#: ../../Instance_Setup/Instance_Setup.rst:97 9d2ca3944f984434b4ecb579b075ccc4 +#: ../../Instance_Setup/Instance_Setup.rst:101 +#: 0bdae3cd584e464ab526840aa032e3a4 msgid "" "**Launch from an existing configuration** (if ``setup.json`` is present):" msgstr "" "**Запуск из существующей конфигурации** (если присутствует ``setup.json``):" -#: ../../Instance_Setup/Instance_Setup.rst:105 -#: d88aa64a570f4f36baf5c7c92d4bd861 +#: ../../Instance_Setup/Instance_Setup.rst:109 +#: ff45af42eb7d4d3faa8640a18bfd61f6 msgid "Configuration File" msgstr "Файл конфигурации" -#: ../../Instance_Setup/Instance_Setup.rst:107 -#: cfb0db3d5e23418d9c93ddb2d34548d4 +#: ../../Instance_Setup/Instance_Setup.rst:111 +#: 39e34a4760354d78b87a8e050029e197 msgid "" "The script writes a file named ``setup.json`` in the same directory. Its " "structure is:" @@ -189,8 +237,8 @@ msgstr "" "Скрипт записывает файл с именем ``setup.json`` в тот же каталог. Его " "структура:" -#: ../../Instance_Setup/Instance_Setup.rst:131 -#: 0a2b6c0e312a49de8ed7838e045b3b66 +#: ../../Instance_Setup/Instance_Setup.rst:135 +#: 9e72e7ecd43c498299e24f5598b6f2b8 msgid "" "**Each list contains at most one object.** When a new server or client " "configuration is added, the entire list for that type is replaced." @@ -198,13 +246,13 @@ msgstr "" "**Каждый список содержит не более одного объекта.** При добавлении новой " "конфигурации сервера или клиента заменяется весь список для этого типа." -#: ../../Instance_Setup/Instance_Setup.rst:136 -#: 1a256391599446d99c3cc375639f251c +#: ../../Instance_Setup/Instance_Setup.rst:140 +#: 8731a083a201487bb579beb4a238a0db msgid "Custom Parameters" msgstr "Пользовательские параметры" -#: ../../Instance_Setup/Instance_Setup.rst:138 -#: 36cefa4820cd42ee93fdb560be1b1032 +#: ../../Instance_Setup/Instance_Setup.rst:142 +#: 1568543fd757484da86f12a72ccb58b5 msgid "" "You can manually edit ``setup.json`` to include any parameter accepted by " "``TCP_Server_Base`` or ``TCP_Client_Base`` (see the source code for the full" @@ -225,13 +273,13 @@ msgstr "" "нужны пользовательские параметры, вам следует добавить их после первого " "запуска или отредактировать файл вручную)." -#: ../../Instance_Setup/Instance_Setup.rst:150 -#: 6b90a320b39147c7ac5287c51b028da3 +#: ../../Instance_Setup/Instance_Setup.rst:154 +#: c62b1bfe8ceb40f8865a34ac87b4ba18 msgid "Extension Protocols and Startup Mode" msgstr "Протоколы расширения и режим запуска" -#: ../../Instance_Setup/Instance_Setup.rst:152 -#: 81cf1090db0240e19aecfb16cdafb622 +#: ../../Instance_Setup/Instance_Setup.rst:156 +#: e6a7dcb4ea184e828e6667ae6505d872 msgid "" "Two extension protocols ship with the launcher and are loaded automatically " "for every instance whose ``setup.json`` entry sets " @@ -241,19 +289,22 @@ msgstr "" "загружаются автоматически для каждого экземпляра, запись в ``setup.json`` " "которого устанавливает ``is_extend_command=True``:" -#: ../../Instance_Setup/Instance_Setup.rst:156 -#: e89028b886064e8ab1e94e5e6a6cdaa0 -msgid "``command_control_extension_tcp.py`` – remote command" +#: ../../Instance_Setup/Instance_Setup.rst:160 +#: 7159f768fc9a4ac199bfe0a4d8478fba +#, fuzzy +msgid "" +"``command_control_extension_tcp.py`` – remote command execution with per-" +"client log collection (``/command``)." msgstr "``command_control_extension_tcp.py`` – удаленная команда" -#: ../../Instance_Setup/Instance_Setup.rst:157 -#: 86a457e4e5bf4791bcbef5bb12612391 +#: ../../Instance_Setup/Instance_Setup.rst:162 +#: d2d277053c74470abc949b891493a5f0 +#, fuzzy msgid "" -"execution with per-client log collection (``/command``). - " -"``forward_extension_tcp.py`` – forwarding messages, files, multiple files, " -"folders and multiple folders to any number of destination clients " -"(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " -"``/folder_forward``, ``/multiple_folder_forward``)." +"``forward_extension_tcp.py`` – forwarding files, multiple files, folders and" +" multiple folders to any number of destination clients (``/file_forward``, " +"``/multiple_file_forward``, ``/folder_forward``, " +"``/multiple_folder_forward``)." msgstr "" "выполнение со сбором журналов для каждого клиента (``/command``). - " "``forward_extension_tcp.py`` – пересылка сообщений, файлов, нескольких " @@ -261,8 +312,19 @@ msgstr "" "(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " "``/folder_forward``, ``/multiple_folder_forward``)." -#: ../../Instance_Setup/Instance_Setup.rst:164 -#: baee7200e5634507a28b2f69309c3c49 +#: ../../Instance_Setup/Instance_Setup.rst:168 +#: a8194174e5fd4dcf87ede716eaada9a4 +msgid "" +"Plain-message forwarding is native to the TCP protocol (no extension " +"needed): the client-only command ``/forward_send_msg`` relays messages to " +"the listed destination clients through the server." +msgstr "" +"Простая переадресация сообщений является родной для протокола TCP " +"(расширение не требуется): только клиентская команда ``/forward_send_msg`` " +"ретранслирует сообщения перечисленным целевым клиентам через сервер." + +#: ../../Instance_Setup/Instance_Setup.rst:173 +#: 98297b76f3404736b92b6c507726a50a msgid "" "With ``is_extend_command=False`` (the default) only the raw TCP protocol is " "started." @@ -270,34 +332,39 @@ msgstr "" "При значении is_extend_command=False (по умолчанию) запускается только " "необработанный протокол TCP." -#: ../../Instance_Setup/Instance_Setup.rst:167 -#: f140a881acba4933bd83af5bb35737d1 +#: ../../Instance_Setup/Instance_Setup.rst:176 +#: 9b5ab0319a9843f1b39721d3a96d1777 msgid "" "The ``is_input_command_in_console`` flag selects how the instance is " "started:" msgstr "" "Флаг ``is_input_command_in_console`` выбирает способ запуска экземпляра:" -#: ../../Instance_Setup/Instance_Setup.rst:170 -#: a40aa238baac4503a0dc0c9cea0745ee -msgid "``True`` (default) – ``start_TCP_Server()`` /" -msgstr "``True`` (по умолчанию) – ``start_TCP_Server()`` /" +#: ../../Instance_Setup/Instance_Setup.rst:179 +#: 1ec730c995b1454da1b208331d9ca8fe +msgid "" +"``True`` (default) – ``start_TCP_Server()`` / ``start_TCP_client()`` is " +"called directly and the console input loop runs in its own thread." +msgstr "" +"``True`` (по умолчанию) – ``START_TCP_Server()`` /``START_TCP_Client()`` " +"вызывается напрямую, и входной цикл консоли запускается в своем собственном " +"потоке." -#: ../../Instance_Setup/Instance_Setup.rst:171 -#: fca5c2c0fe914740a931957b30a9927b +#: ../../Instance_Setup/Instance_Setup.rst:182 +#: 3cfb2eba312943cea85388f4e9faf40d +#, fuzzy msgid "" -"``start_TCP_client()`` is called directly and the console input loop runs in" -" its own thread. - ``False`` – the instance runs in a background thread and " -"the launcher keeps the process alive until the instance stops (useful for " -"headless deployments)." +"``False`` – the instance runs in a background thread and the launcher keeps " +"the process alive until the instance stops (useful for headless " +"deployments)." msgstr "" "``start_TCP_client()`` вызывается напрямую, и цикл ввода консоли выполняется" " в своем собственном потоке. - ``False`` — экземпляр работает в фоновом " "потоке, и средство запуска поддерживает процесс до тех пор, пока экземпляр " "не остановится (полезно для автономного развертывания)." -#: ../../Instance_Setup/Instance_Setup.rst:177 -#: c2925f0f85154040bcdd1252a228fd7f +#: ../../Instance_Setup/Instance_Setup.rst:186 +#: 3b3482fa903f4afd81234e588a229c1f msgid "" "Both extensions also expose injectable registration " "(``setup_server_commands(instance)`` / ``setup_client_commands(instance)``) " @@ -313,62 +380,67 @@ msgstr "" " принимает существующий экземпляр, поэтому несколько расширений можно " "загрузить в один и тот же экземпляр из кода." -#: ../../Instance_Setup/Instance_Setup.rst:186 -#: e58290a2c5f3492bafa47f15b7c2958f +#: ../../Instance_Setup/Instance_Setup.rst:195 +#: 1dc7b46c0e3047d2885573cc4f55e7c6 msgid "Internal Operation" msgstr "Внутренняя операция" -#: ../../Instance_Setup/Instance_Setup.rst:188 -#: 6655764b01644b92910338c01d38b0cb -msgid "Each instance is launched in a new terminal window" +#: ../../Instance_Setup/Instance_Setup.rst:197 +#: 8870bc6b731d40a095a40faed0c8f16d +#, fuzzy +msgid "" +"Each instance is launched in a new terminal window (or background process)." msgstr "Каждый экземпляр запускается в новом окне терминала." -#: ../../Instance_Setup/Instance_Setup.rst:189 -#: 6824028a99254c77a55fb56d51b5e9a5 -msgid "(or background process)." -msgstr "(или фоновый процесс)." - -#: ../../Instance_Setup/Instance_Setup.rst:190 -#: 0a09353648d44ba2a840fc1cf8a850bc -msgid "The configuration is passed via a temporary JSON" +#: ../../Instance_Setup/Instance_Setup.rst:199 +#: 13c720a93ba54e8ca330fce3a078bb5b +#, fuzzy +msgid "" +"The configuration is passed via a temporary JSON file to avoid shell " +"escaping issues." msgstr "Конфигурация передается через временный JSON." -#: ../../Instance_Setup/Instance_Setup.rst:191 -#: f6327281e35a4304b404563d00301a1a -msgid "file to avoid shell escaping issues." -msgstr "файл, чтобы избежать проблем с экранированием оболочки." - -#: ../../Instance_Setup/Instance_Setup.rst:192 -#: 997e364f35714b18b1e47af641ce58db -msgid "If an instance fails to start, the error is" -msgstr "Если экземпляр не запускается, возникает ошибка" - -#: ../../Instance_Setup/Instance_Setup.rst:193 -#: 4404c088296e4c35b49ed59eca3b6678 -msgid "displayed and the window pauses for inspection." +#: ../../Instance_Setup/Instance_Setup.rst:201 +#: 9566eb3cb70e4b77bcbdb584f7e0b6fc +#, fuzzy +msgid "" +"If an instance fails to start, the error is displayed and the window pauses " +"for inspection." msgstr "отображается, и окно приостанавливается для проверки." -#: ../../Instance_Setup/Instance_Setup.rst:196 -#: 2b8f982006184d36b06d4ac5579a36f4 +#: ../../Instance_Setup/Instance_Setup.rst:205 +#: 5f0f0f6a41384356986a1182f14a0a4c msgid "Requirements" msgstr "Требования" -#: ../../Instance_Setup/Instance_Setup.rst:198 -#: f10fabd9883942b780cd0822da245837 +#: ../../Instance_Setup/Instance_Setup.rst:207 +#: 49ceb74e41104b00971cd9edecef08ae msgid "Python 3.6+" msgstr "Питон 3.6+" -#: ../../Instance_Setup/Instance_Setup.rst:199 -#: f464e1a571fa46388937f3236b36d3bf +#: ../../Instance_Setup/Instance_Setup.rst:208 +#: 541cd1be44c044f3880eb220a87c06b7 msgid "The ``network_api.connect_tcp`` module must be" msgstr "Модуль ``network_api.connect_tcp`` должен быть" -#: ../../Instance_Setup/Instance_Setup.rst:200 -#: fc6ca227cd904333916e81b4a7093acc +#: ../../Instance_Setup/Instance_Setup.rst:209 +#: 427fef8d7c5047fc934681026969639a msgid "importable (the script imports ``TCP_Server_Base``" msgstr "импортируемый (скрипт импортирует ``TCP_Server_Base``" -#: ../../Instance_Setup/Instance_Setup.rst:201 -#: dcc9c410853247b0a3302c2c41f6fe13 +#: ../../Instance_Setup/Instance_Setup.rst:210 +#: fb35af261d664664bdb3c1b3aaac3e83 msgid "and ``TCP_Client_Base`` from there)." msgstr "и ``TCP_Client_Base`` оттуда)." + +#~ msgid "``True`` (default) – ``start_TCP_Server()`` /" +#~ msgstr "``True`` (по умолчанию) – ``start_TCP_Server()`` /" + +#~ msgid "(or background process)." +#~ msgstr "(или фоновый процесс)." + +#~ msgid "file to avoid shell escaping issues." +#~ msgstr "файл, чтобы избежать проблем с экранированием оболочки." + +#~ msgid "If an instance fails to start, the error is" +#~ msgstr "Если экземпляр не запускается, возникает ошибка" diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.add_extension.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.add_extension.po new file mode 100644 index 0000000..c51b367 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.add_extension.po @@ -0,0 +1,91 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.add_extension.rst:2 239fa5edd0d944bd884b21d803e0f4a1 +msgid "PyFlow.add\\_extension module" +msgstr "" + +#: PyFlow.add_extension.copy_extension_files:1 c4a96a18a6754f1fb4c5c3a9ff9dd518 +#: of +msgid "Validate extension path(s) and return them as a list." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst 4b8e1285572947c4a34dbcd9e22fb52c +#: 78eb00a995d84b028e95127f753b4fb5 PyFlow.add_extension.remove_extension +#: dff4aabd6aad43e188c9fc9b73d9142b of +msgid "Parameters" +msgstr "" + +#: 49c2e6f1496d433ab7a9d162802419f1 5de3fc7a955443cb9bef23851780925f +#: PyFlow.add_extension.add_extension:3 +#: PyFlow.add_extension.copy_extension_files:3 +#: PyFlow.add_extension.remove_extension:3 b82537ecca1e4f418a5a6aacacdd900c of +msgid "a single path string or a list of path strings." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst fa91fb96a98f4374b2da5085d1373ef4 +msgid "Returns" +msgstr "" + +#: 450992bbbe91432fb44b0265e58d8f59 PyFlow.add_extension.copy_extension_files:5 +#: of +msgid "The original paths as a list (extensions are not copied)." +msgstr "" + +#: 487ed0fee55540c796c92b88d0a8b2ea +#: PyFlow.add_extension.add_added_extension_logs:1 of +msgid "Append paths to the extension registration log file." +msgstr "" + +#: PyFlow.add_extension.add_extension:1 cd718206d465487ebd43c17e796c4da5 of +msgid "Register extension file(s) in added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.remove_extension:1 bb4313796aff49a1b96a37d66912f18a of +msgid "Remove registered extension path(s) from added_extensions.json." +msgstr "" + +#: 6520ce551e5f40e6afe59c549d15e493 PyFlow.add_extension.remove_extension:5 of +msgid "If the registration file does not exist, this is a no-op." +msgstr "" + +#: 26339cf4e8a041f798d969a890592971 +#: PyFlow.add_extension.load_registered_extensions:1 of +msgid "Load every registered extension from added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.load_registered_extensions:3 +#: e53cceb4fe384c1fa10ddf1905998823 of +msgid "" +"For each registered path, the module is imported dynamically and its " +"``setup_server_commands(instance)`` or " +"``setup_client_commands(instance)`` is called, depending on " +"*instance_type*." +msgstr "" + +#: 11efbd89b0254bb190c21005396f53dd +#: PyFlow.add_extension.load_registered_extensions:7 of +msgid "" +"Raises ImportError if the JSON file is reachable but a module cannot be " +"imported or loaded, or if the required setup function is missing." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po new file mode 100644 index 0000000..0173b3c --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po @@ -0,0 +1,37 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.command_control_extension_tcp.rst:2 +#: a6084f2941f34f579f28c55e9dfb768d +msgid "PyFlow.command\\_control\\_extension\\_tcp module" +msgstr "" + +#: 9209095513ec402980427a0ddd988219 +#: PyFlow.command_control_extension_tcp.setup_server_commands:1 of +msgid "Register the control-extension commands on a server instance." +msgstr "" + +#: 114b0a54c29747fd88bc2852eab174bf +#: PyFlow.command_control_extension_tcp.setup_client_commands:1 of +msgid "Register the control-extension commands on a client instance." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.flow_setup.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.flow_setup.po new file mode 100644 index 0000000..2e96013 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.flow_setup.po @@ -0,0 +1,67 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.flow_setup.rst:2 a6a54598744740409fe0b28f73834ffe +msgid "PyFlow.flow\\_setup module" +msgstr "" + +#: 586460f36f3b4654abb0db6b5d77b39d PyFlow.flow_setup.launch_web_tool:1 of +msgid "Launch the transfer_web launcher (``kind`` = \"server\" or \"client\")." +msgstr "" + +#: PyFlow.flow_setup.launch_web_tool:3 ef1274ba70124aeeadee3d4cfcca99c2 of +msgid "" +"The web tool is a Flask app that opens a browser UI, so it runs in its " +"own process (a terminal window when one is available, otherwise detached)" +" and the launcher returns immediately." +msgstr "" + +#: 7086887643164f229f919546dceb0e36 PyFlow.flow_setup.edit_existing_instances:1 +#: of +msgid "Vim-style editor to delete/change existing instances." +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:3 d04ffe1cf853403292908fc69962f7dc +#: of +msgid "Returns (status, servers, clients):" +msgstr "" + +#: 5a751731515c4adfba9787fdd5e93215 PyFlow.flow_setup.edit_existing_instances:4 +#: of +msgid "status == \"saved\" -> setup.json was written (:w / :wq); keep the" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:5 e61a6437dae14d1e9f9ac4e6848cd444 +#: of +msgid "returned edited lists" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:6 bb9424a8ea894dbb888df4fa2e0875ed +#: of +msgid "status == \"discarded\" -> the editor was exited without saving" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:7 d0d81a426f754475922c3e9f4aee6627 +#: of +msgid "(:q! / :q) and the original lists are returned unchanged" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po new file mode 100644 index 0000000..09e0268 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po @@ -0,0 +1,146 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.forward_extension_tcp.rst:2 +#: 39b0ca283c7c48b08f66a67eb80766b0 +msgid "PyFlow.forward\\_extension\\_tcp module" +msgstr "" + +#: PyFlow.forward_extension_tcp:1 ad6b75f791c543dea48894cd8bac6941 of +msgid "Forward extension for the TCP protocol." +msgstr "" + +#: PyFlow.forward_extension_tcp:3 ff4264b5fba24d7d93e9bb3e398edfd5 of +msgid "" +"Disk-based, upload-then-push forwarding of files and folders to a list of" +" destination clients. This is deliberately a second implementation of " +"file forwarding: the native TCP protocol already streams files and " +"folders in memory (``/forward_file`` / ``/forward_folder`` on a client " +"console, relayed by the server as ``/forward_item`` with no disk I/O on " +"the server), while this extension uploads the data to the server's " +"transfer directory first and then asks the server to push the stored " +"copies. Plain-message forwarding is native as well (the client-only " +"command ``/forward_send_msg``, relayed by the server), so no string " +"forwarding lives here." +msgstr "" + +#: 2f49002886224f5bb68eb09f2c4a8a30 PyFlow.forward_extension_tcp:14 of +msgid "Transfer families added by this extension:" +msgstr "" + +#: PyFlow.forward_extension_tcp:16 ab1c45d0c28345c2898a54c66c98416b of +msgid "/file_forward <(ip, port)> ..." +msgstr "" + +#: 93ef2915b1fa4bd9b4b42e8e6ec747f8 PyFlow.forward_extension_tcp:17 of +msgid "forward one file to every listed destination" +msgstr "" + +#: 9773fb5cd9414f0aabb496ae3a32005a PyFlow.forward_extension_tcp:18 of +msgid "/multiple_file_forward ... <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:19 ee54881259274d6b9085bf71b44d659a of +msgid "forward several files to every listed destination" +msgstr "" + +#: 19144093ca8d45e988dfcfe09221bf6a PyFlow.forward_extension_tcp:20 of +msgid "/folder_forward <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:21 b73d9263c9874b1e9ea5a714138d72d8 of +msgid "forward one folder (structure preserved) to every destination" +msgstr "" + +#: 4f7e8bb4b31c4eddbb78d815e85f8795 PyFlow.forward_extension_tcp:22 of +msgid "/multiple_folder_forward ... <(ip, port)> ..." +msgstr "" + +#: 9e95e860d39744f8ba3d52ddaf357943 PyFlow.forward_extension_tcp:23 of +msgid "forward several folders to every listed destination" +msgstr "" + +#: 612162f14a574132bf396a1b6ea853f1 PyFlow.forward_extension_tcp:25 of +msgid "" +"Items come first, destinations last; every destination is written as a " +"Python address tuple, e.g. ``\"('127.0.0.1', 3000)\"``. There is no limit" +" on the number or size of items or destinations." +msgstr "" + +#: 1b0ec7328493432fb876117b1f76bb3b PyFlow.forward_extension_tcp:29 of +msgid "" +"The commands are only available on the client console: they are " +"registered in the \"client\" handler group, so typing them on the server " +"console is rejected as an unrecognized command. Forwarding goes through " +"the server - the client uploads the data over the normal transfer channel" +" (the server stores it in its transfer directory) and then asks the " +"server to push it to the destinations, which receive it through the main " +"protocol's own receive paths. Destinations that are unreachable (not " +"connected to the server, or the server itself, which is never in the " +"client table) are skipped and the remaining destinations are still " +"served." +msgstr "" + +#: 443d9fe7a21148d984a0e3f2f3cb2c28 +#: PyFlow.forward_extension_tcp.setup_client_commands:1 of +msgid "Register the file/folder forward commands on a client instance." +msgstr "" + +#: 5dbc099afeb54fbb859bf5760331adfd +#: PyFlow.forward_extension_tcp.setup_client_commands:3 of +msgid "" +"Message forwarding (``/forward_send_msg``) is native and needs no setup. " +"Each command binds its transfer kind and single/multiple policy into the " +"shared handler via functools.partial; where_to_run=\"client\" makes them " +"fire from console input only." +msgstr "" + +#: 392556d0850f44b1b64dae0fe65a748c +#: PyFlow.forward_extension_tcp.setup_server_commands:1 of +msgid "Register the file/folder forward relays on a server instance." +msgstr "" + +#: 399725c6728441568702225abc50b2c4 +#: PyFlow.forward_extension_tcp.setup_server_commands:3 of +msgid "" +"The message relay (``/forward_send_msg``) is native and needs no setup. " +"These handlers are triggered by relay requests sent by clients, i.e. they" +" live in the \"server\" group: messages coming in from other instances " +"are dispatched there. The /xxx_forward commands themselves stay in the " +"client group, so typing them on the server console is rejected as " +"unrecognized." +msgstr "" + +#: 337c29db84fe4d3099a2e3d5e135d0d1 PyFlow.forward_extension_tcp.client_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable client (mirrors the control " +"extension)." +msgstr "" + +#: 99ee11955438459d84f5c1ae6f3fedbb PyFlow.forward_extension_tcp.server_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable server (mirrors the control " +"extension)." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po new file mode 100644 index 0000000..1f75b18 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po @@ -0,0 +1,1760 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_tcp.rst:2 +#: 1e4cd09ce3ef45ceb95b8eb81eb7493b +msgid "PyFlow.network\\_api.connect\\_tcp module" +msgstr "" + +#: 25b88cdb337c44539a843c674ba6825b PyFlow.network_api.connect_tcp:1 of +msgid "" +"TCP transport for PyFlow: the server and client base classes and the wire" +" parsers." +msgstr "" + +#: 1f1482fc5c2849a68f34c74a8b55ab79 PyFlow.network_api.connect_tcp:3 of +msgid "" +"``TCP_Server_Base`` accepts connections and dispatches inbound lines; " +"``TCP_Client_Base`` connects, sends and reads on the same conventions:" +msgstr "" + +#: 76e93df1b95547ff8bded710dc31340d PyFlow.network_api.connect_tcp:6 of +msgid "" +"one message per line, terminated by a newline; a line that starts with " +"``/`` is a command and goes to the command handlers, anything else is a " +"plain message reported to the registered message listeners;" +msgstr "" + +#: 2497ecf21ac947c78502a3839a452cb8 PyFlow.network_api.connect_tcp:9 of +msgid "" +"an RSA-encrypted channel is negotiated right after connect unless " +"``is_enable_encrypto`` is False;" +msgstr "" + +#: 9702905f1f8a4750905b2ffb96f99ec8 PyFlow.network_api.connect_tcp:11 of +msgid "" +"file/folder transfer, message forwarding and port allocation are layered " +"on the same socket and share its command namespace." +msgstr "" + +#: 4cc5743bdfe54148ad38f54f10e688c9 PyFlow.network_api.connect_tcp:14 of +msgid "" +"The forwarding extensions use the module-level parsers " +"`parse_forwarded_message`, `parse_forward_items_and_addrs`, " +"`parse_forward_originator` and `forward_skip_message`." +msgstr "" + +#: 49796529cb7f49ee8131b257212b5420 PyFlow.network_api.connect_tcp:18 of +msgid "" +"Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and " +"``TCP_Client_APIs.rst``; argument, return and exception contracts live in" +" the docstrings below." +msgstr "" + +#: 39274682c4bd46489ed6fe535b50ede5 +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:1 of +msgid "Split a ``/send_msg_from `` relay envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07da14a120314eb7a3acd1c46ddfeaca 0b005c95146946208a01640628f50a57 +#: 10904afb15ee45b29e91a2ab5da0db26 14d9353e52a54c019428fb94211f17f6 +#: 1861e8bcd5bb41489f13265860311349 1c6ba1e0968a409499b7a262c34dcc2a +#: 20b95160870648538fd617ca4ce3d2b5 2351fc654dfc452a8d07e5d566265d9f +#: 27611fb8240a422db411c043f60bdbe9 464f4d62231e4a61b1a939ad2054b13c +#: 48bd66821c14451d9a0c682d9468a5a5 4e1c4be3e22f403396d5ee1788d0e3b0 +#: 4fa9ba6e763440b8ab00832520b4d305 501a0caef71d436e84469bc1ef9c1e5f +#: 52bdf8d502cc4a31a6df5ae543ceb182 531f1e6ff68e44bcac61633e2d7c511f +#: 5404b8d0f66848c3b1db9f08a25f92c0 55fc240d149746e190ecf924a2ca6dd4 +#: 59ab6a93eceb40f7b590133c3d2b8548 6d8d8704b9c94105928dbbdf15ce9f12 +#: 70d6dae11a144ed7b7e9d8e16dbf478c 71de1bb0c4d24062a819a6fe59013d34 +#: 74c64b0da3294322b512f3155955fb7a 76f08e34cd4a42c880880cfb511fcc66 +#: 7a6a9f1497ac48f5867783d78fdae37c 7c3501da0be44d78a671e96ba4384489 +#: 7e9c7fe1ae974c1082550fb3e6e3de1f 7fc19de66098437faef7bffed3b5f752 +#: 7fdf476d99ff4f54ad35cf4bb506e47d 812d9d039fe34f71aa4b662c4511c8e9 +#: 81f8f7d346ff45639f39eb0f033103df 95d8b328540c4094a4fcc8fdc9139645 +#: a163536aaa1f49399502da52fb481666 acdf8b3b266f4eccb6ceca17a110603b +#: b097aecf6ae74e17b36fc9e806d5b26a b3db16a6c2374980ae9b071d9f3f15e6 +#: c2a18448d83f4879bcb29f51ca31bc5b c5ed2382d01042c68a5e372a4c7de2ac +#: d36c8c10afba4732969367886b8663ab d6d746e16dbd47308eea5aacd9614f15 +#: d8e94668c5d7434fa61c2ffcbe73b6af dad1324e962d49419245fe3b88c20121 +#: dd3d356c14cb462e98ea42e3502dca80 eb24a57758194ab2bdef9e95362584df +#: ef5da8d6c91c469fad0871d213999d2a ff3615b7096e46bba8797b739bb954b2 +msgid "Parameters" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:3 +#: e42a6166c82d4861b25cb55f587db28c of +msgid "Received line, e.g. ``/send_msg_from ('127.0.0.1', 3000) hello``." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07f0651994f343d7ac8ee26ae4a45c8e 0b14e887dc524d55affce5a3bd9b9f8d +#: 0cb557ccd8a6423091d0d562f1af31fd 0d1ef94754104f01a11302d72600e73a +#: 1475f9cb68fe417b9de7226406e810b5 14dcda8b059b480982e3fb03399e665b +#: 1d7731490c2f49959618924ddf4327fd 1e40a9184f8544c6b7a1472d55c66a11 +#: 24af9c4562134d16b50c664902b39dff 295718cd92e04380af38e7afaff04010 +#: 34fc7f7760d94c2f8bac262ff821b4d7 451b693e9c414cff9798a5a1592fe9cd +#: 47bbee4bd36c4f998fd04c8cc8d9c199 537ed27e714148da8ea804e3562ccd96 +#: 565b9def256a453784c749fe2ee93bc8 57dee3931014419c91d049fca185ef1b +#: 5dcfbd3c4b13401a82e19373602a4b92 635677a7aff64b79a4f69ffb4c22841c +#: 74b8a4ad713a43f2a14faf494e1886a4 7eca27378af64ce99374c606219ff337 +#: 7f6560a3959c4f069adff9d00ca33f54 855ad029af764579bf4ab14cbd430caa +#: 967377f151e84130a9778ece6228bec2 99035a24f6e94207b84545f8c71a451e +#: a90e5d6a51cd4591afeb3cd934071de0 c72891376fe2481d95c7d9ae3014ae47 +#: ca270cee6d3f4eb7ba331ab841c77f38 cb62202c244d4c08974c674c13a02dca +#: db47465f222c4f3db50a9133de7035f5 eb66bb58c7534346b105203e37b25760 +msgid "Returns" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:7 +#: f25d1962656c4f339805427e2573f84f of +msgid "" +"``(sender_id, payload)`` where ``sender_id`` is the sender's " +"``\"ip:port\"``, or None when the line is not a well-formed envelope." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:9 +#: d0d7af37e4134902ae64e10b2e43b4c1 of +msgid "``(sender_id, payload)`` where ``sender_id`` is the" +msgstr "" + +#: 09b385f0a98640c981d8563427e44d7e +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:10 of +msgid "" +"sender's ``\"ip:port\"``, or None when the line is not a well-formed " +"envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 0acb7e2291644625b8873e997a801b95 140c4a59eb0f43d5b4dc46fc2d3348a7 +#: 1412375394164f17aa5b40b7d9ecf0a4 19eb6fca79324b0791f8ec7488a5246c +#: 21c016ab955e4e6499a97f0ca86684cb 27bd312392a2404099fa44fd8787ed28 +#: 27dcdca3b6b247069ddce775ec471c70 28f7c9db48d949ef930abcedef021138 +#: 28fa8dcc5913460e8906e926c95408b9 2e363ed3ce6746c08ebfbf8bb889b623 +#: 3d654cd4b6b044baa3774f758d61da71 48e6af3ee30f47409e2837867a01ff48 +#: 50da78ed5f904c74a876d7b494241efa 50df226290614747b5568f633a883ae0 +#: 564af23329ae4b29aaea3a3638292f42 571f3b412784479a9bfdcb5034c6a39f +#: 6759afd1efe445adb4970c6265c95995 73e666bc0d164cb6a1506517d9acfdb4 +#: 752e44acb3104f9bbf4d3ff7c8bc244b 76862b0024bd42e2b012c999d92e6969 +#: 7ba45226839e40c8a00e684e7c4e07b9 7cd20d28b6794f34a0f53570fea9546a +#: afead614116b49619f1d29738a18e166 b443d67ba649468ca55f1889f18dd006 +#: bbc7b22bbf5e42f7bae149f481de9f3a c58a75656c6143f8a0cb397f24b619b2 +#: d4f12b17df174b038d554053e034cd2a e669e2ac035448fab6479e209ab51c4d +#: f278b3632fbc43f2b4cbeb7759606a68 f6ee270c707f4805b9035fa772078d19 +msgid "Return type" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:1 +#: f22254d8ca2d47ad8bc9c98664369d61 of +msgid "Split forward-command tokens into items and destination addresses." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:3 +#: b207f60051004834bab91ba392a88437 of +msgid "" +"A token of the form ``('ip', port)`` is a destination, everything else is" +" a forwarded item (message text or a path). Used by the native message " +"forwarding (``/forward_send_msg``) and by the file/folder forward " +"extension." +msgstr "" + +#: 5329fb72f5954768a6cec05ff4cccfea +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:7 of +msgid "Tokens after the command name." +msgstr "" + +#: 2aebf669048046ddb279a935bcfcfde4 +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:10 of +msgid "" +"``(items, addrs)`` in the order given; ``items`` holds texts and " +"paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: 96b96bd19fe94312bddf340fad073cbc +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:12 of +msgid "``(items, addrs)`` in the order given; ``items`` holds texts and" +msgstr "" + +#: 8e82954621d748e2b412952a7fb2753b +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:13 of +msgid "paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:1 +#: a51324658c7943158d90ed706cecc41d of +msgid "Build the console notice for a forward destination that cannot be served." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:3 +#: b541e8446e374bc8b38cd7912a1fa33c of +msgid "Destination ``(ip, port)`` that is unreachable or is the server itself." +msgstr "" + +#: 10e03f21d05e478d9cde0c7176f6e1b8 +#: PyFlow.network_api.connect_tcp.forward_skip_message:7 of +msgid "One-line notice for the console." +msgstr "" + +#: 8005e532fe9d491ca8ddd996889d605d +#: PyFlow.network_api.connect_tcp.parse_forward_originator:1 of +msgid "Extract the originator's ``\"ip:port\"`` from a received transfer command." +msgstr "" + +#: 2cf3052a89c645e782430f1f05fb37a1 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:3 of +msgid "" +"The server's forward relay tags every pushed ``/file`` and " +"``/file_folder`` command with the forwarding client's address tuple; a " +"direct send carries the receiver's own address instead." +msgstr "" + +#: 335f6c1b16104395ba38a33188943d8a +#: PyFlow.network_api.connect_tcp.parse_forward_originator:7 of +msgid "Received transfer command." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:9 +#: e5bd1024ad3f47f19cafc4c83a844b60 of +msgid "" +"This instance's own ``\"ip:port\"``; a command carrying it is a direct " +"send and yields None." +msgstr "" + +#: 65469492f844448fab269b701cbdb704 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:13 of +msgid "" +"Originator ``\"ip:port\"``, or None when the command carries no " +"originator (direct send or non-transfer command)." +msgstr "" + +#: 04a0882a3a5e4099a114728b4d1c79c0 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:15 of +msgid "Originator ``\"ip:port\"``, or None when the command carries no" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:16 +#: a2d34276c4f54c69981f9b51f36cc31d of +msgid "originator (direct send or non-transfer command)." +msgstr "" + +#: 7297b41c47664243b9677b32e21de07e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:1 of +msgid "TCP server: accept clients, dispatch commands, relay messages and files." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:3 +#: f5119fd32b60425bb33e4aa148ff7139 of +msgid "" +"Each accepted connection is served by `handle_client` in its own thread: " +"a line starting with ``/`` goes to `handle_command` (built-in commands " +"plus the handlers registered with `register_command`), any other line is " +"a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``." +msgstr "" + +#: 2870b0163d68487daf7526e91eb49ff6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:11 of +msgid "Address the server socket binds to." +msgstr "" + +#: 007c3d5fac544fd480ac10cc56c2f236 0929a7309fef40cab69e9dc22c376ef2 +#: 2e995e9363c54a7ba267ff3a53e10908 507062a8930e432dbcab31b039724b27 +#: 66f4496879ee419d8d748faf8cff0f05 75fcffd8750942559c93c5e590a10503 +#: 83c05847c82b492090a9279f5072434d 8e64315489764ceba6e1120da4675ffb +#: 9c28c79f3bb84af98029a17c0ffc84cd +#: PyFlow.network_api.connect_tcp.TCP_Client_Base +#: PyFlow.network_api.connect_tcp.TCP_Server_Base +#: cf754e752b6c445989b3301b807fc9eb d61fb222783b4f898eb1e757631a1c9a of +msgid "type" +msgstr "" + +#: 2dcb98253fbf428b96fdb5b720769c02 568be916ae0340289a6569e718e7cbf2 +#: 904732a26bcc4835bd3429f0015ae8a4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:14 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:26 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:13 of +msgid "str" +msgstr "" + +#: 6ac74cca7b0f443ca89f3d2c3bbe1aaa +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:17 of +msgid "First port considered for binding and for allocation." +msgstr "" + +#: 0f9991451920470fa3c8a74228b46bb0 9ecf88c5193c470b98dc95d184c94f92 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:19 of +msgid "int" +msgstr "" + +#: 6746407aac194c4880c7cbf82b7fa2fb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:23 of +msgid "" +"Accepted connections keyed by ``(ip, port)``; each value holds " +"``socket``, ``address``, ``id`` and ``connected_time``." +msgstr "" + +#: 7ba2aaf3fe4c42739121eb3ca2bcc79a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:26 of +msgid "dict" +msgstr "" + +#: 93556efee2aa445684af3fdc1532548a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:30 of +msgid "True while the accept loop runs." +msgstr "" + +#: 0ab0667a38254203929e173ab5024f1d 76745b91cce345b092b4e5d931d71126 +#: 839584ae46a0488a95917b35e253768a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:38 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:44 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:32 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:38 +#: f692af2051ae43cdae389a97cd91e9d7 of +msgid "bool" +msgstr "" + +#: 4ad873e4e9da4574bc068029455f62ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:36 +#: fae1c5fe3a844b69a2c7ec611073ecf9 of +msgid "Whether the RSA channel is negotiated." +msgstr "" + +#: 37e6dd691f544a80b9e0949883a2e5a8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:1 of +msgid "Create the server and, unless extended, start accepting clients." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:3 +#: bf58d54a167d45de9a18641877ada912 of +msgid "Address the server socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: 2f4d0c4bfd8246d4984d4c843510775d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:6 of +msgid "First port to bind; also the base of the allocation range." +msgstr "" + +#: 48b1e43c9f2a4e81a9c760ca0d80bad0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:8 of +msgid "Maximum concurrent clients. Defaults to 10." +msgstr "" + +#: 8786a9d2b6b449209be813778d785712 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:10 of +msgid "Step between candidate ports. Defaults to 1." +msgstr "" + +#: 15c0118df8da4454a7fea8d529989237 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:12 of +msgid "Number of ports per step. Defaults to 100." +msgstr "" + +#: 973382c423524d44a569c318399e0b99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:14 +#: d5f8f341d98d478289d9a226f37c1003 of +msgid "Concurrent file transfers allowed. Defaults to 10." +msgstr "" + +#: 4fe9a7607dbc4eebbce4e62f9306c76f +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:17 of +msgid "" +"Reserve a port range across processes, so several instances on one host " +"do not collide. Defaults to False." +msgstr "" + +#: 8aa56addc6ae4a7790dc957d1e1c2b60 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:20 of +msgid "Start the console command thread. Defaults to True." +msgstr "" + +#: 242566ff64584fba88e4fed8bb189110 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:28 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:23 +#: b6d5c80f7a8e4358889f1d90f9a579a4 of +msgid "" +"Worker slots for `submit_task` and threaded command handlers. Defaults to" +" 10." +msgstr "" + +#: 87d1cd7d6569498b8af8bc7064235784 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:26 of +msgid "" +"When True, do not call `start_TCP_Server`; the caller starts the server " +"when ready. Defaults to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:29 +#: ac43634ddb1a4d15a9f3d0f58d011441 of +msgid "" +"Negotiate the RSA-encrypted channel for every connection. Defaults to " +"True." +msgstr "" + +#: 1ffccca56902418ba98ee0c6776ad8fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:37 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:32 +#: e0eb36563dc5479a92ce7555eb5a7965 of +msgid "" +"``[pub_key_path, pvt_key_path]`` pair used instead of the default key " +"lookup; an invalid pair is ignored." +msgstr "" + +#: 420bbdf8d6bb400fbd3548bea03354ac +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:35 of +msgid "" +"Buffering ceiling in MiB for the in-memory forward pump; past it the " +"uploader is told to pause. Defaults to 2048." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 012113b84d6e439db2b772b810838e0a 57443c71b0b84d20b3cf755ee7118632 +#: 63ab5a2f2327475ba324e245f8e6e2ac 7cfe1a15e78d4c2d91a46599dc106667 +#: 8b33b297e4e64f2ba7463207ef17cc68 a4cb07de3c4d4a5c959c403938ed51e6 +#: a85e930c77364c9eaac1c8c9e9e2c740 c79723467b3a4fa1a7ddcb1038fd8414 +msgid "Raises" +msgstr "" + +#: 37d7580e55904c278f12fe0f31fe0d3d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:46 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:39 +#: ed59ec69b9a1430597e6d8758051aff5 of +msgid "" +"If the ``.Flow`` directories or ``decode_command_table.json`` cannot " +"be created or read." +msgstr "" + +#: 810bc26336b24ad1acba3f56d466f8e9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:1 of +msgid "Reserve this server's port range under the cross-process lock." +msgstr "" + +#: 5540b6efb5864c188e080ec7aad5c3ca 9e68e81f7a184af8a8fe90557dbaa5de +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:3 of +msgid "No-op unless ``is_hand_alloc_port`` is True." +msgstr "" + +#: 356ed2e8333d49b98ed6834612b4b56d 4a28dc37e6b74154b3790646ec8abfa7 +#: 5e267cb8af8d49e38ddaeabd8a7e9b57 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:6 +#: daf116d5b2264b1a88c836c1ad5192a3 of +msgid "Step between candidate ports." +msgstr "" + +#: 28bfef7616a1498db2b8827758175f68 7e663e1ae24640d1b856b1f9a0525380 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:8 +#: e18848eedc5d4d8abd97d73a0984a5d3 f81918ab77304aeb99302710e3062db2 of +msgid "Number of ports per step." +msgstr "" + +#: 11e0523d1b194702a07a92748468da25 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:1 of +msgid "Release this server's reserved port range." +msgstr "" + +#: 801c063c5fa447bda1dbf7f5ccbc6a9a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the server port range for this process." +msgstr "" + +#: 672bb7a9496c43ac8b706a3d0062ddfd +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:1 +#: of +msgid "Report whether the server port range is reserved by some process." +msgstr "" + +#: 19c63440e9ff4dadb6a3614ef17ef055 39423e052b70403ba07a713aa9e3cb4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:3 +#: of +msgid "True while the lock file exists." +msgstr "" + +#: 5b6cf828ffb24e098a0c86a09818da73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_unlock:1 +#: of +msgid "Remove the lock file that reserves the server port range." +msgstr "" + +#: 479bdf5518dc4a029ecf47ad44bdcd5d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:1 of +msgid "Allocate the next free server port range and record it on disk." +msgstr "" + +#: 34c085a1bfca4dedafc749bfdcf14ad5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:3 of +msgid "" +"``port`` is moved past the ranges already recorded by other servers, so " +"the instance ends up with a range of its own." +msgstr "" + +#: 197b30e318d741c2b1f8afc925e91e5c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:11 of +msgid "If the server port info file cannot be read or written." +msgstr "" + +#: 1c97c8ef484b42ab86d36b3742bf3879 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_free_port:1 of +msgid "Drop this server's entry from the on-disk port range record." +msgstr "" + +#: 9fc07e857fa048e2956e64bf78f6e386 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:1 of +msgid "Allocate a transfer port, waiting until one is free." +msgstr "" + +#: 505d7b9fb1024bc8ba886e5b0a38fbf2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:3 of +msgid "" +"Allocated port, or 0 when allocation is disabled " +"(``is_hand_alloc_port`` False)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:5 +#: e85fdf7f7a594e0b9aed565b4a3ab42f of +msgid "Allocated port, or 0 when allocation is disabled" +msgstr "" + +#: 153357eb91794cb692467afe5e94b41c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:6 of +msgid "(``is_hand_alloc_port`` False)." +msgstr "" + +#: 26c5c3768e1446e78c9efc5f8038b23d 280a1814abb049cbb26998bd681609ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:1 of +msgid "Release a port obtained from `palloc`." +msgstr "" + +#: 0e0379de669843778f6827973c29372e 3ee789104c4749b1a5749730be83a046 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:3 of +msgid "Port to release." +msgstr "" + +#: 6a8d4efd9f9a45e399525e2f169035ee +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:1 +#: f9203c3ebcb24fd0b5110110b8693b9c of +msgid "Allocate the next port above the base, or the first free one in range." +msgstr "" + +#: 73e9e2ddd451402a93d10dbac6ac9374 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 9d96a242eab8433ca750d670a5f63b82 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:5 of +msgid "Allocated port; None when the upward range is exhausted; 0 when" +msgstr "" + +#: 7b05e18fccbe4ab081d3cd22d59a48e0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:6 +#: ebba1d1151cc4d5991f783986cd0f480 of +msgid "allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 5f15d2ac36eb47a2b23fb93de93755c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:1 +#: f2d9abe435cf49f6825bb72a68013934 of +msgid "Release a port obtained from `file_palloc` and step the cursor back." +msgstr "" + +#: 6c1fb07a212d4bf19717407b0c7184d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:3 +#: c933ad6d68ec4c5d9bb5afc88c386fdd c9edcb78694a444fb0f2cc5f72db01fa +#: e32e165133154b9791e35f117f8a85a9 of +msgid "Port to release. Ignored when allocation is disabled." +msgstr "" + +#: 1afd81c685234a20aa41c5cde93d327d 7f5db505a5fa4c03a56e9ac5ff6aba74 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:1 of +msgid "Allocate the next port below the base, or the first free one in range." +msgstr "" + +#: 3fc63f6d51aa4c0fb22fd0f945f9cfd7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 74a441ccfa5e421dadd215ef0725a25f 9b5089cb85494c069d408e344db75d9d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:5 of +msgid "Allocated port; None when the downward range is exhausted; 0 when" +msgstr "" + +#: 183b4335eebc41058759e30b97477018 9c4b9bddd98c463f9e140bd2d8425dad +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:1 of +msgid "Release a port obtained from `spy_palloc` and step the cursor back." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:1 +#: c66c76cab8564da4b6b6cb855fd63fd5 df9eb5ed5e844a6a8f00e77be5c032e3 of +msgid "Register a custom command handler." +msgstr "" + +#: 85787fb3d66340eb9920983dd95e03c6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:3 +#: cdec2f24d2dc47bc8c505b7472dc3bf7 of +msgid "" +"Command to intercept, e.g. \"/my_command\"; matched case-insensitively " +"against the first token." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:6 +#: c7c29a3836c9493e9c8804cc5896144a of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back to the sender as the response." +msgstr "" + +#: 7c64d0c0b9ac44b995811a0fdaa0dfaf +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:10 of +msgid "" +"\"server\" for commands arriving from clients, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 7d2a7d91ad9743b29110a43100444097 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:13 +#: a0c074a9da664c5eb5fa63deb6765c36 of +msgid "" +"Run the handler on the worker pool instead of the reader thread. Defaults" +" to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:17 +#: ce0ad78e00b24bb8b675ce8d715b8323 e6c616f887334cb6925c03de2ad6f64c of +msgid "" +"False when ``where_to_run`` is neither \"server\" nor \"client\"; the" +" handler is then not registered." +msgstr "" + +#: 3a440bf70b36447e82577535a85baca1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:19 +#: f125f1386b434f3a8199b1ceb8c44eca of +msgid "False when ``where_to_run`` is neither \"server\" nor" +msgstr "" + +#: 1a68dac068db434eb76e28015005f29c 7c7784e8bc9f4a76b12b6be2d3df9285 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:20 of +msgid "\"client\"; the handler is then not registered." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:1 +#: ef5d20f1a16945368383a8cdbc3daa19 of +msgid "Register ``listener(client_id, message)`` for every inbound plain message." +msgstr "" + +#: 12cdf8300e934ace8498d00d39501ba2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:3 of +msgid "" +"Plain messages are the lines received from clients that do not start with" +" ``/``; commands go through the registered command handlers instead." +msgstr "" + +#: 9ec5fafe8ded489785400e3e489b8573 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:6 of +msgid "" +"``listener(client_id, message)`` where ``client_id`` is the sender's " +"``\"ip:port\"``. It runs on the receive thread, so it must not block, and" +" exceptions raised inside it are swallowed." +msgstr "" + +#: 3a640e40aeeb43938e65b49a2cd3dbba +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:1 +#: edb8edc2eb3f433186b54807a8ac43ad of +msgid "Unregister a listener previously added by `add_message_listener`." +msgstr "" + +#: 0336bd7428fa4f688cce21bb6a4156fe 6fd7bb75a29e4973ac2bd05119255039 +#: 977594c4b1074688958a018c03d9ee5b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:3 +#: b59375740438476aa73d6f19c2240ce6 of +msgid "Listener to remove; an unknown one is ignored." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:1 +#: cf95477e5794404fbdf4e877a132ba1c of +msgid "" +"Register ``listener(client_id, full_path, name, size, command)`` per " +"saved file." +msgstr "" + +#: 9c57d09f6d3d437890ae3ad306071701 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:3 of +msgid "" +"Fired after a file uploaded by a client (a direct send, or a forwarded " +"file/folder item staged on the server) has been fully written to " +"``file_transfer_dir``." +msgstr "" + +#: 6c938b7c8be3464f9925527b6deb4e1d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:7 of +msgid "" +"``listener(client_id, full_path, name, size, command)``; ``client_id`` is" +" the uploader's ``\"ip:port\"`` and ``command`` the wire command that " +"triggered the transfer, so a listener can recognise protocol pushes such " +"as ``/crypto_pub_key``. It runs on the transfer thread, so it must not " +"block." +msgstr "" + +#: 549734db517346ec814fa83ec2f46f00 57b1e98125974187b986386da7991dd0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:1 of +msgid "Unregister a listener previously added by `add_file_listener`." +msgstr "" + +#: 58f1ccd078ea406dab00d6d4be886a76 7a6c25f715d14b0aaf4ec7009db9fc3c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:1 of +msgid "Run a callable on the instance's worker pool." +msgstr "" + +#: 6e05c7f46b244ae195c32ee52797fa37 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:3 +#: a51df9d8fc924d468d8f6d8a61dfff12 of +msgid "Callable to run." +msgstr "" + +#: 6f9eee14b6cb4daf82acfbba1e34e873 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:5 +#: e1c79706c9414895bdf7bf183bf5d41a of +msgid "Positional arguments forwarded to ``func``." +msgstr "" + +#: 9221436fa83a4831942cf5be4e37e2cb 993bdb8fbe3240d3b1032014576182fe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:7 of +msgid "Keyword arguments forwarded to ``func``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:10 +#: a6b26d6cf54d4fafac2950cbe33ba56f df538e45ae7f4bd19398fd3d063a4744 of +msgid "" +"Handle for the submitted call; its worker slot is released when the " +"call finishes." +msgstr "" + +#: 214ffa9d5cb242d1b7a5f76c0da16cd6 60f19983f7384aefbb6c97b84dd25f93 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:12 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:12 of +msgid "Handle for the submitted call; its worker" +msgstr "" + +#: 942e2e90fc904344b71b0c2d9c39c9f6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:13 +#: eadf6af0a0954798933dc427ba8e107e of +msgid "slot is released when the call finishes." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:1 +#: a2284b241a28498892e5a59158961b2e fece61298a354d6db119309b29c6751d of +msgid "Start a temporary listener for a side channel (not the main protocol)." +msgstr "" + +#: 01d42ba32c2b41f482dd077cfb950f9b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:3 +#: b7455f1055b7459ca8eee2b5052eb808 of +msgid "" +"``handler(client_socket, address)`` started in its own thread for every " +"accepted connection." +msgstr "" + +#: 5e1b84445ee34c9cac00e1a2c9ef297b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:6 +#: c76bc24facf8407babf9023ef19c3879 of +msgid "Port to bind; None allocates one with `palloc`." +msgstr "" + +#: 4855e3ca5a3a41dfa37eb32a8d6c8d9c 9c69c4dd1d904078af48c6308ba35895 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:8 of +msgid "Listen backlog. Defaults to 1." +msgstr "" + +#: 29148f4a3160460f8dc27fd1e64c0a31 47f98fd15e6144bd845d175cd039a6c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:11 of +msgid "" +"``(port, thread, stop_event)``; setting ``stop_event`` ends the loop," +" which closes the socket and frees the port." +msgstr "" + +#: 91f803f6aa944d07a57cff61d1071efe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:13 +#: ecf3d32c44db40a8a13545d623ffe2c9 of +msgid "``(port, thread, stop_event)``; setting ``stop_event`` ends the" +msgstr "" + +#: 6ca9f2a625634468a15171916ec6d6ea +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:14 +#: abce9546b7444c6383c9020c4133ec63 of +msgid "loop, which closes the socket and frees the port." +msgstr "" + +#: 04767fd6d11641ac88c41c78867609f5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:17 +#: a270cfabdac64296b8a33ad4d662896f of +msgid "If ``port`` is None and no port can be allocated." +msgstr "" + +#: 031d3e96394c454093561375a377dcce 7e534c863e4a4275adf9a3a93dd5cbc0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:1 of +msgid "Open a temporary outbound connection for a side channel." +msgstr "" + +#: 112fda54e1eb4981b4272df61c5416e2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:3 +#: f9f4af8e25654b4ca38700b9faf635ef of +msgid "Host to connect to." +msgstr "" + +#: 26edcce3a1c24d5d95ca0b2a86cdff36 59d3c414b4304673be5d3232d2291680 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:5 of +msgid "Port to connect to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:7 +#: c5766672f68f47c9908e68d2ed3be7f3 of +msgid "Local port to bind; None lets the OS choose." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:9 +#: bfc1b713e9fc4fe797e50c357f472e76 c0e20701686a4ea68dcce54795822508 of +msgid "``on_data(data, client_socket)`` called for every received chunk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:13 +#: c563d04fcd7a41f1a2aeace42974430a cbd0df6f67a64a5580227be38a10020a of +msgid "" +"``(client_socket, thread, stop_event)``; setting ``stop_event`` ends " +"the receiver thread." +msgstr "" + +#: 0e1b9679f3104a9caab601e6bc7c0905 4fbec161e0c84ef1ba95379314846158 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:16 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:15 of +msgid "``(client_socket, thread, stop_event)``; setting ``stop_event``" +msgstr "" + +#: 14528f9f11144ced9b5822d2640fe2a9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:16 +#: c76fce92c3f944c1857df89bbc134540 of +msgid "ends the receiver thread." +msgstr "" + +#: 5ea5fd66c3d3434292e4d8e1dd1da094 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:1 of +msgid "Send one message to every connected client." +msgstr "" + +#: 32945f04e0564f9d88495506f0ab4f90 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:3 of +msgid "Clients whose send fails are disconnected and removed from ``clients``." +msgstr "" + +#: 00de0b1f60b14189b1629f8146db68a6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:5 of +msgid "Payload passed to `send_message`." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:7 +#: a91c8f6921c7435e990516c2134efcf8 of +msgid "``(ip, port)`` to leave out, typically the client the message came from." +msgstr "" + +#: 9d57c62f1fd94c1dacfe32aa50cc9665 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:1 +#: of +msgid "Send the messages of a console line to the clients named in it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:3 +#: e7fbf4bbd84b4920a2b9591fabcaf2d9 of +msgid "" +"``/send_msg`` line as typed: message text followed by one or more ``(ip, " +"port)`` identifiers; each message is delivered to the identifiers that " +"follow it. Addresses that are not connected are skipped with a console " +"notice." +msgstr "" + +#: 9d4819fe4ca94a1b81c9e0423ff98212 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:1 of +msgid "Write one line to a client socket, encrypting when the channel is up." +msgstr "" + +#: 1c7b88e29a0143a39e13441769186ef4 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:3 of +msgid "Target connection." +msgstr "" + +#: 2d32d96ad70e41d893b15bfa90c0864b 6f8bd9adaf2b4c44a9446b049144fe21 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:5 of +msgid "" +"Payload; a str is stripped and newline terminated, bytes are sent as they" +" are." +msgstr "" + +#: 5c03983cb460497fb72f126c1d8a2bc1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:9 of +msgid "" +"True when the payload was written, False for an unsupported payload " +"type." +msgstr "" + +#: 0b11a149117c4a1fb6bcf2fba3d1a515 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:11 of +msgid "True when the payload was written, False for an unsupported" +msgstr "" + +#: 602950a9f5ad48ea92ab60aea3b26488 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:12 of +msgid "payload type." +msgstr "" + +#: 149f767d72dc4da7b1d88fed768d2474 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:15 of +msgid "If the server is not running or no socket was passed." +msgstr "" + +#: 07dac2d20d7041779c8b189d45ee2e64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:16 of +msgid "If the socket write fails (the original error is re-raised)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:1 +#: caf2377ee7f348ab8c95bf2a27c44be4 of +msgid "Read up to ``msg_length`` bytes from a client socket." +msgstr "" + +#: 214af2667d0f443eb16c0a4aaf723b2e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:3 +#: cf94d4a352744a61a1f781128fae9694 of +msgid "Connection to read from." +msgstr "" + +#: 2b0cd669c9994324be7570e59a04677d 7c3eb31687804ba5a59acc140ba50230 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:5 of +msgid "Maximum number of bytes to read." +msgstr "" + +#: 51eade0145d74a2bbfc51070bc779703 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:8 +#: ad9dfdd3ef56445181d7204b7b5bfa15 of +msgid "Received bytes, empty when the peer closed the connection." +msgstr "" + +#: 7103cfcec26c44999cb5464251ee461c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:1 of +msgid "Serve one accepted client until it disconnects." +msgstr "" + +#: 65f9de7f44774aa6aef81fcbe4dc1c64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:3 of +msgid "" +"Registers the client, greets it, announces the encryption mode and reads " +"lines until the peer closes: commands go to `handle_command`, plain " +"messages go to the message listeners and to ``messages_dict``. Runs in " +"its own thread; the client is removed from ``clients`` and the socket " +"closed when the read loop ends for any reason." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:9 +#: db3915757e2842c8afee97b2d471411e of +msgid "Accepted connection." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:11 +#: fae378e888be4821a14ed2483cfde1e0 of +msgid "Peer ``(ip, port)``; used as the client id and as the key in ``clients``." +msgstr "" + +#: 32886e2babcf4283924034c600453c4a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:1 of +msgid "Dispatch one command line received from a client." +msgstr "" + +#: 8972b8c4edf149fd9a4008832c85ed31 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:3 of +msgid "" +"Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, " +"``/crypto_mode``, ``/file``, ``/file_folder``, " +"``/server_file_transfer_port`` and the crypto exchange lines) are handled" +" here; any other name goes to the handlers registered for the \"server\" " +"side via `register_command`. An encryption-mode mismatch closes the " +"connection; an unknown command is only reported on the console." +msgstr "" + +#: 208294924333477a8e7d65aa3130777e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:10 of +msgid "Connection the line came from." +msgstr "" + +#: 306e5026cfc1441d8c754f0e02866cf3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:12 of +msgid "Peer ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:14 +#: a2a3a1fd57e14260a53675457fdff0b4 d87b7a9e04464b7ea4f979d46b42932b of +msgid "Line including its leading ``/``." +msgstr "" + +#: 8d727a2aaab14beaa1b6858b59b0821d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:17 of +msgid "" +"Response for that client, or None when no response is due (crypto " +"lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:19 +#: ed04a3bf285b4f57bb6a5de0c4f7284f of +msgid "Response for that client, or None when no response is due" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:20 +#: c4ca8e9c3e9941e1b33b5e0af2c13bab of +msgid "" +"(crypto lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:1 +#: a784dfdc5ded411d985426e36f6e5435 of +msgid "Send one plain message to a connected target, tagged with its origin." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:3 +#: b545977244dc402aa8586264d2d933f3 of +msgid "" +"Public API for forward extensions: the message is wrapped in a " +"``/send_msg_from `` envelope so the receiver can " +"attribute it to the originator (see `parse_forwarded_message`)." +msgstr "" + +#: 8ee5275fbf894581ade9e6f30d5d4aff +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:7 +#: ee97948978e64b099947a0716f27d581 of +msgid "Destination ``(ip, port)``." +msgstr "" + +#: 38ffd8a7079d4794811e3504c01052f8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:9 of +msgid "Payload to deliver." +msgstr "" + +#: 3b48d6e3b84b4dde9a09f3c2f07c8b61 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:15 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:13 +#: cf0f9590c44e40309471c70ebd2e433d e2207f0b24bf42c984505e0de8c8f8bf of +msgid "Originating client ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:14 +#: d2471a90e269464c977239f406051120 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the envelope was sent." +msgstr "" + +#: 1ae8a7060df7423895175f210441a399 320d99b8923f4eaa886ffe0a16bfc6d9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:24 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:16 of +msgid "False when ``target`` is not connected (a console notice is" +msgstr "" + +#: 1a97f59b270f404096d9c2b37ab09a8c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:17 of +msgid "printed); True when the envelope was sent." +msgstr "" + +#: 6baab06dc12e4ecc80d1a16c26c2a19a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:1 of +msgid "Build the tagged wire command that pushes one forwarded item." +msgstr "" + +#: 0c3f9dcbcf8f413fbd5befefb960c402 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:3 of +msgid "" +"Public API for forward extensions. The originator tuple sits before the " +"trailing transfer id, where the receiver's existing parsers ignore it and" +" `parse_forward_originator` recovers it for attribution." +msgstr "" + +#: 3cf58937a2d04172bf544119279d85c1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:7 +#: a5ccb57e63d144c2844910f8ecf2faa4 of +msgid "\"file\" or \"file_folder\"." +msgstr "" + +#: 621a93603c8549c0ba047bee6aae5a38 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:9 +#: a20ab65e584c4daf9a9153008b713ee1 of +msgid "Relative folder path (folders only)." +msgstr "" + +#: 410039575afd4797a0eb1be9098ccda5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:11 +#: f0f56fb642874c3889fb0dacfed4f4ef of +msgid "File or folder name." +msgstr "" + +#: 214d3e0a88fa41d8a02af0a5d09c5ac6 72d2b6fdd324482eaea9803def1fe2da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:15 of +msgid "Transfer id shared by the pushed item." +msgstr "" + +#: 0aa1bd5b053544e98ae4ab3ae1add5f7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:17 +#: e7826b64fe174bf0a6f7faa4df49f491 of +msgid "Receiver-side destination directory." +msgstr "" + +#: 93e666db45e745619b90c744e1646b73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:20 of +msgid "Command line to hand to `send_message`." +msgstr "" + +#: 56d8327bb7414a5e85292f8f2ace9d42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:1 of +msgid "Push one forwarded file or folder item to a connected target." +msgstr "" + +#: 3098afa7f4ac43a184d081dda8faa6ba +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:3 of +msgid "" +"Public API for forward extensions: sends the line built by " +"`forward_target_command`, which the receiver attributes with " +"`parse_forward_originator`." +msgstr "" + +#: 9b9634061040430484cd2596f43a8feb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:22 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:25 +#: b8bb8c432f7649d5a35480fec0509a51 of +msgid "printed); True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:1 +#: b03f4edeca5f4cc19f6d695aac3690de of +msgid "Bind the server socket, then accept clients until `stop` runs." +msgstr "" + +#: 7e027ea659ab48e19ea346cb284fe144 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:3 of +msgid "" +"Blocks the calling thread. A console command thread is started when " +"``is_input_command_in_console`` is True, every accepted connection gets " +"its own `handle_client` thread, and a client beyond ``max_clients`` is " +"refused with a message. Socket errors and the end of the accept loop both" +" end in `stop`." +msgstr "" + +#: 291f5a8cc4f648ee9910e5cd45074675 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:1 of +msgid "Read console commands until the server stops." +msgstr "" + +#: 1ba2e428d432496688ef23f1c07562da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:3 of +msgid "" +"Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, " +"``/file_folder``, ``/multiple_file_multiple_client``, " +"``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward " +"commands are client-only and are refused here. Any other name goes to the" +" handlers registered with ``where_to_run=\"client\"``. Ctrl-C and EOF " +"stop the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:1 +#: eb90f49c77e64006a0c9ae861ff849b1 of +msgid "Stop the server and release everything it owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:3 +#: a059b1e71106487999c83c2e2042f509 of +msgid "" +"Closes the server socket and every client connection, flushes the message" +" and event stores, releases the allocated port range and clears " +"``running``. Safe to call more than once." +msgstr "" + +#: 32b06ea315b3404caa3a05c4aed0ce64 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:1 of +msgid "" +"TCP client: connect to a server, dispatch commands, send and receive " +"messages." +msgstr "" + +#: 2b30f5c9ecd24fc89cdcd58000ee1e99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:3 of +msgid "" +"Lines received from the server go through `receive_messages`: a line " +"starting with ``/`` is handled by `handle_server_command` (protocol " +"commands plus the handlers registered for the \"server\" side), any other" +" line is a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``. With " +"``is_input_command_in_console`` the console thread `interactive_mode` " +"sends typed lines to the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:12 +#: b9532c86786c43d5aea58b24157fefed of +msgid "Server address this client connects to." +msgstr "" + +#: 2cfa1e3e1674407ba1e2683e76fef65d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:18 of +msgid "Server port this client connects to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:24 +#: af04fdee2c95431a9b0c17f96e50c18c of +msgid "Local address the socket binds to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:30 +#: b1d4b5606e1c48b6a915815527542758 of +msgid "Local port, None when the OS chose one." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:32 +#: d926b4f3eb4b40a4acb21f780a4dc227 of +msgid "int | None" +msgstr "" + +#: 500cfd177cf5453d887f6904b73b7851 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:36 of +msgid "True while the connection is up." +msgstr "" + +#: 40f2a9e1b730422db3dd58bc2a6046e3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:1 of +msgid "Create the client and, unless extended, connect and start reading." +msgstr "" + +#: 0552aa36b7b74474af8f43b380dbee54 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:3 of +msgid "Server address to connect to; required before `connect` is called." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:6 +#: d246ff9aaf154bfa91b5c4c9d293afd5 of +msgid "Local address the socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:9 +#: e981b68ebbea46e9b556b09f4b5cd44a of +msgid "Server port. Defaults to 65432." +msgstr "" + +#: 766bf4c138234fe081bbd2668dcb2a5d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:11 of +msgid "Local port to bind; None lets the OS choose an ephemeral port." +msgstr "" + +#: 5d661c0f91f64ef39cacb1a5466d752c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:14 of +msgid "" +"Socket timeout in seconds for connect and receive. Must be None when " +"``is_wait_server`` is True." +msgstr "" + +#: 9478d02062284ecaa7838f931ab80780 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:17 of +msgid "Step between candidate ports in the allocation range. Defaults to 1." +msgstr "" + +#: 2e1695ae510b43758793257124fd23c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:22 of +msgid "Enter interactive mode after connecting. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:25 +#: e02da86957dd4f45bf84ede4e9c82cda of +msgid "Keep retrying while the server is not reachable. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:31 +#: e1fb7f67e1714a61a9e8fe500555c700 of +msgid "" +"When True, do not call `start_TCP_client`; the caller connects when " +"ready. Defaults to False." +msgstr "" + +#: 1634dbe84a95421a92a660f7c6fdd10e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:34 of +msgid "Negotiate the RSA-encrypted channel with the server. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:40 +#: cff66a7ac1ad410f96ed1ee03cb1cf53 of +msgid "" +"Buffer ceiling in MiB, kept for parity with the server class; the " +"client's forward path does not read it today. Defaults to 2048." +msgstr "" + +#: 0f06ff3b6db940da96ec09bee3dba526 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:45 of +msgid "If ``is_wait_server`` is True and ``timeout`` is not None." +msgstr "" + +#: 5dd677b9627d4b84b53dc7a6b2848b3f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:6 of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back as the response." +msgstr "" + +#: 0a57a7875bb94767904ff8d93fa77eb8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:10 of +msgid "" +"\"server\" for commands pushed by the server, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 876e8fe2a6d645829ba12ddbad5006df +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:1 of +msgid "Register ``listener(sender_id, message)`` for every inbound plain message." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:3 +#: d166a2cc8ea04bed82c5f6dd61dcdcf6 of +msgid "Mirrors the server-side contract; commands are not reported here." +msgstr "" + +#: 412834182c9442adaf0b85e24c200ccc +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:5 of +msgid "" +"``listener(sender_id, message)``; ``sender_id`` is the author's " +"``\"ip:port\"`` — the forwarding client for a message another client " +"forwarded here (``/send_msg_from`` envelope), or None for a direct push " +"from the server, which names no client author. It runs on the receive " +"thread, so it must not block, and exceptions raised inside it are " +"swallowed." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:1 +#: fc8a83e0b5f84273a8287b122c1a9a3e of +msgid "" +"Register ``listener(full_path, name, size, command)`` per saved inbound " +"file." +msgstr "" + +#: 34e3386853564aab9a824e96438fc3e5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:3 of +msgid "" +"Fired after a file pushed by the server (a direct send, or a forwarded " +"file/folder item) has been fully written to ``file_transfer_dir``." +msgstr "" + +#: 63f925709b83433e967610672b6fbc79 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:6 of +msgid "" +"``listener(full_path, name, size, command)``; ``command`` is the wire " +"command that triggered the transfer, so a listener can recognise protocol" +" pushes such as ``/crypto_pub_key``. It runs on the transfer thread, so " +"it must not block." +msgstr "" + +#: 0e91f82d8856443497c399f97643b757 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:1 of +msgid "Reserve this client's port range under the cross-process lock." +msgstr "" + +#: 6029a28126344e45b24394cb56b2f8f8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:3 of +msgid "No-op until the server assigns a range (see ``/client_alloc_port_range``)." +msgstr "" + +#: 32bed401c3574959ba5ed08fc9078401 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:1 of +msgid "Release this client's reserved port range." +msgstr "" + +#: 9f86b7aac4f44886be9cd87b56072b63 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:3 of +msgid "No-op unless a range was assigned (``is_hand_alloc_port`` True)." +msgstr "" + +#: 8154f96d9ec74bd588971a806d42f8c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the client port range for this process." +msgstr "" + +#: 69c2dd5d5d1d497c8bac1e62ab4fc30a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:1 +#: of +msgid "Report whether the client port range is reserved by some process." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_unlock:1 +#: c88f37ef2d5c47bda346e804508d6ec0 of +msgid "Remove the lock file that reserves the client port range." +msgstr "" + +#: 478443e187814a6ca7ad798277db023f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:1 of +msgid "Allocate the next free client port range and record it on disk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:3 +#: a8736540f4c3402fad9508e619d559d7 of +msgid "" +"``port`` is moved past the ranges already recorded by other clients on " +"this host, so each instance ends up with a range of its own." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:11 +#: bc44b1604e02467280b59cb8ad63d0af of +msgid "If the client port info file cannot be read or written." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_free_port:1 +#: e48dff576a074109a135db168b5dbe98 of +msgid "Drop this client's entry from the on-disk port range record." +msgstr "" + +#: 3aa62e8843c6495c862f43fc6dda9d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:1 of +msgid "Allocate a port, waiting until one is free." +msgstr "" + +#: 2f7752fa51d946b1a4a566b38f046e80 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:3 of +msgid "Allocated port, or 0 when no allocation range was assigned." +msgstr "" + +#: 503ab40fc32c4d548f65fa3272b2f4d2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:5 +#: b9fd549c95f24c2080d87cec92514db6 of +msgid "Allocated port; None when the upward range is exhausted; 0 when no" +msgstr "" + +#: 151a65f1c1984e4196c750108d0611cb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:6 of +msgid "allocation range was assigned." +msgstr "" + +#: 0c6201bb2d7e45d3a4916d7e50a21e10 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: 76c766a9c61f4075a762ccf55038a65e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:6 of +msgid "no allocation range was assigned." +msgstr "" + +#: 8d500513e8854ba4bb91c8905a9adb73 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:7 of +msgid "Local port to bind; None allocates one with `palloc`." +msgstr "" + +#: 840360623c8a4c2bbafe8fe7bbf9209c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:1 of +msgid "Connect to the server and start reading from it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:3 +#: df94288e660b48f6809358b3dc234ec9 of +msgid "" +"Binds ``client_port`` when one was configured, then retries while " +"``is_wait_server`` is True and the server is not reachable yet. Once the " +"socket is up the receive thread is started and the encryption mode is " +"negotiated, which closes the connection when the two sides disagree." +msgstr "" + +#: 22fccba0ab36499a9a7f4427abe5e9f4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:8 of +msgid "" +"True when the connection is established (and, if encryption is " +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 0270d0a2d42740a9889106339d9f2dbb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:10 of +msgid "True when the connection is established (and, if encryption is" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:11 +#: c194691cedfb48c6bfd97d9bdc6f2246 of +msgid "" +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 4dc38e3ef3b74b8e8c27378242a1925a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:1 of +msgid "Read from the server until the connection ends." +msgstr "" + +#: 431649a0d703476dadc6afa8adbe53d4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:3 of +msgid "" +"Runs on the receive thread: plain lines are reported to the message " +"listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes " +"are attributed to their sender first), other ``/`` lines go to " +"`handle_server_command`. Any end of the connection clears ``running`` and" +" releases the port range." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:1 +#: cf284b59f7f849039404ced20f8e12db of +msgid "Write one line to a socket, encrypting when the channel is up." +msgstr "" + +#: 40ba2ede2d3f4666b82107af129d24d0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:3 of +msgid "Target connection; the client passes ``self.client_socket``." +msgstr "" + +#: 2e663941eaf142cb96738add05f9ef4d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:10 of +msgid "" +"True when the payload was written; False when the client is not " +"running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: 38905ec7539d42e0839907c32dd8667f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:12 of +msgid "True when the payload was written; False when the client is not" +msgstr "" + +#: 3f7e2e6fedda476f996132c3f677f2f0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:13 of +msgid "running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:1 +#: a9220a130e694e0aba1d3b6320dff498 of +msgid "Send the payload of a console line to the server." +msgstr "" + +#: 2e208e83a89e4dac9793460a8808a042 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:3 of +msgid "" +"Console line such as ``/send_msg hello``; the first token (the command " +"name) is dropped and the second one is sent." +msgstr "" + +#: 39c8e70ca92b43b8ac687bc0aa7f4073 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:7 of +msgid "If the line has fewer than two tokens." +msgstr "" + +#: 8d47f30ae7b0451f8ecfe207ec835ff3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:1 of +msgid "Read up to ``msg_length`` bytes from a socket." +msgstr "" + +#: 10a39005f45c46728ef9a000eeaf9109 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:1 of +msgid "Dispatch one command line pushed by the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:3 +#: a7e781fde939475cb081204adac16ded of +msgid "" +"Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the" +" connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange " +"lines, and the transfer lines ``/file``, ``/file_folder``, " +"``/forward_upload``, ``/pause_trans``, ``/start_trans``, " +"``/forward_error``. Any other name goes to the handlers registered for " +"the \"server\" side via `register_command`; an unknown command is only " +"reported on the console." +msgstr "" + +#: 91942d88960c4793b5eab8c3654c1400 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:1 of +msgid "Forward plain messages to other connected clients through the server." +msgstr "" + +#: 75bb9147a7464a278e19af9d03b04ecb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:3 of +msgid "" +"The console command ``/forward_send_msg`` uses this; the client must be " +"connected. The server wraps each message in a ``/send_msg_from`` envelope" +" so the receiving client can attribute it back to this one." +msgstr "" + +#: 0f3c201936df45b687dc5532531b4a4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:7 of +msgid "Message texts to forward." +msgstr "" + +#: 277bbcb2143943b5b983badb00c3f4fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:9 of +msgid "Destination ``(ip, port)`` tuples." +msgstr "" + +#: 5ea8eea489fc43108a439d54fba31068 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:12 of +msgid "" +"True when the request was written to the server; False when the " +"client is not connected." +msgstr "" + +#: 2bc5d7e5dfa94eb29b49f12562f57c88 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:14 of +msgid "True when the request was written to the server; False when the" +msgstr "" + +#: 45d974ad946b408b91d41bcce65abcc8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:15 of +msgid "client is not connected." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:1 +#: c9401da7c37e40e5978c80e4876b11a0 of +msgid "Read console lines and act on them until the client stops." +msgstr "" + +#: 793bcfd02c014200b0ec47f8e15b4d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:3 of +msgid "" +"``/quit`` closes the connection; ``/send_msg``, ``/file``, " +"``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, " +"``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are " +"handled locally; any other name goes to the handlers registered with " +"``where_to_run=\"client\"``, and anything left is sent to the server as " +"it stands. Ctrl-C and EOF close the connection." +msgstr "" + +#: 6dbae803a56c442588b28af664b7a0e9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_file_console:1 of +msgid "" +"/forward_file ... ... [dest] (client " +"only)." +msgstr "" + +#: 05cad8b03e0747dc804ffbfda3122fe1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_folder_console:1 of +msgid "" +"/forward_folder ... ... [dest] " +"(client only)." +msgstr "" + +#: 6e9a9717a782419baecca63fd1f46baf +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:1 of +msgid "Close the connection and release everything the client owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:3 +#: ba6c5badecf6465db6036735870ed305 of +msgid "" +"Stops the receive loop, releases the port range, flushes the message and " +"event stores and closes the socket. Safe to call more than once." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:1 +#: c91a3245b93f46abbb49dbef7bcfeb1c of +msgid "Connect to the server and start the client loop." +msgstr "" + +#: 0c83552867484ea780fcff8d34e5c2d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:3 of +msgid "" +"Enters `interactive_mode` when ``is_input_command_in_console`` is True, " +"otherwise keeps the process alive while the connection is up. Exits the " +"process with status 1 when the connection cannot be established; Ctrl-C " +"and the end of the connection both run `close`." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po new file mode 100644 index 0000000..4521edf --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_udp.rst:2 +#: c777b065237a4eb1993c041388639a1d +msgid "PyFlow.network\\_api.connect\\_udp module" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.po new file mode 100644 index 0000000..c68efe4 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.po @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rst:2 9abd125493904de2bb64c9158a11243f +msgid "PyFlow.network\\_api package" +msgstr "" + +#: ../../api/PyFlow.network_api.rst:10 7012029060714904ba5d281c1d607be9 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po new file mode 100644 index 0000000..7b4ae95 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po @@ -0,0 +1,233 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rsa_crypto.rst:2 +#: def9740b5a2e4aaeb2aeed8b4f02c0ec +msgid "PyFlow.network\\_api.rsa\\_crypto module" +msgstr "" + +#: 46d3090efafd4504870cf72a4bea22ab PyFlow.network_api.rsa_crypto:1 of +msgid "crypto_api (C/OpenSSL) RSA integration for PyFlow's TCP layer." +msgstr "" + +#: 5b1ff4a00b0f46258cda2e7440368de1 PyFlow.network_api.rsa_crypto:3 of +msgid "" +"A thin ctypes binding to the shared ``libcrypto_api`` plus the key " +"lifecycle required by the encrypted TCP channel:" +msgstr "" + +#: PyFlow.network_api.rsa_crypto:6 a5a0d6d600604683990a92555a49a3fe of +msgid "" +"Reuse an existing RSA keypair from ``~/.ssh`` (PEM private key) when one " +"is present and parseable, otherwise generate a fresh keypair into " +"``.Flow/pvt_key``. A caller-supplied keypair (``custom_keys``) is " +"honoured when both files parse and the pair matches." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:10 bb413dc908624589a3e57f8a2c702728 of +msgid "" +"Anti-MITM identity check (TOFU): every connection exchanges public keys " +"in plaintext. Each side records the peer in " +"``.Flow/pub_key/pub_key.json`` under the peer's ``(ip, port)`` with the " +"SHA-256 of its public key; a later connection from the same endpoint " +"presenting a different key is rejected, and a known key seen from a new " +"endpoint is re-registered under the new ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:16 aec1a7998276428881efcfac6fe4cebe of +msgid "" +"RSA-OAEP encrypt/decrypt with the ``_VALID`` plaintext signature so a " +"stale key (for example a rotated ``~/.ssh`` pair) is detected and the " +"peers re-exchange their public keys." +msgstr "" + +#: 60edbdead17c422782e688ab68f8a6d1 PyFlow.network_api.rsa_crypto:20 of +msgid "" +"The C library must be built first (``cmake -S . -B build && cmake --build" +" build``); see ``load_library`` for the search paths." +msgstr "" + +#: 67af7d5c5f41474fba915352e506a8b4 +#: PyFlow.network_api.rsa_crypto.CryptoLibraryError:1 of +msgid "Raised when the shared libcrypto_api cannot be loaded." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaKey:1 e620122ff0bc48dd985c4b6a0b3b41ce of +msgid "Owns an ``pf_rsa_key_t*`` handle; frees it on GC." +msgstr "" + +#: 1a7d8e9db5a3435486d716b13f2a2a56 +#: PyFlow.network_api.rsa_crypto.load_library:1 of +msgid "Locate and load the shared crypto_api library (cached)." +msgstr "" + +#: 670f4a719bf24c24b48ecb5d73b373ce +#: PyFlow.network_api.rsa_crypto.get_local_mac:1 of +msgid "Return a stable 48-bit machine identifier as colon-separated hex." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.get_local_mac:3 +#: f1306bb875d34b558f7418278fded413 of +msgid "" +"Uses ``uuid.getnode()`` (the real hardware MAC when one is available). " +"Server and client on the same host share this value; the ``_`` " +"prefix in the key file names keeps them apart." +msgstr "" + +#: 333763d798c54d52a9a56a3e4e3e2155 PyFlow.network_api.rsa_crypto.RsaCrypto:1 +#: of +msgid "Key lifecycle plus RSA-OAEP encrypt/decrypt for one role." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto:3 f52515121ac74575aaa36d45dd0341a2 +#: of +msgid "" +"``role`` is ``\"server\"`` or ``\"client\"`` and is used to name the " +"locally generated keypair (``pvt_key/_priv.pem``) and the peer key " +"cache (``pub_key/__.pem``). Peer identity is tracked" +" in ``pub_key/pub_key.json`` (TOFU, see ``verify_peer_pub``)." +msgstr "" + +#: 56fed5bcfe194b7f90a0c662ae36d6c0 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:1 of +msgid "Create the crypto wrapper for ``role`` (\"server\" or \"client\")." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:3 +#: ca4e27664ccf40ce935d991db43f7c69 of +msgid "" +"``custom_keys`` may be a ``[pub_key_path, pvt_key_path]`` pair to use a " +"user-supplied RSA keypair instead of the default lookup (``~/.ssh`` / " +"generated). The pair is validated on first use (paths exist, files parse," +" the keys match); an invalid pair is ignored and the default lookup is " +"used instead." +msgstr "" + +#: 7d89122ff0f54090b67209e3b08ae29c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:1 of +msgid "Load the RSA keypair (see module docstring) and cache handles." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:3 +#: d5e494e00be44891b33cb8e0ecfb8081 of +msgid "" +"Runs under ``_key_lock``: the private-key handle must never be replaced " +"(or freed on GC) while another thread is decrypting." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.reload_own_key:1 +#: f6f7d6ec8837464dad2dc647e6141a99 of +msgid "Re-read the private key (e.g. after a ~/.ssh rotation)." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:1 +#: e95d18da34d14376987a62f6cbbec778 of +msgid "" +"Path of the exchanged public key file for ``peer_role`` at ``(peer_ip, " +"peer_port)``." +msgstr "" + +#: 52dc6fb74f0c4175bfa2a2acc23cda0d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:4 of +msgid "" +"The IP is sanitized for the filesystem (``:`` -> ``_`` so IPv6 literals " +"are safe on every platform, including Windows)." +msgstr "" + +#: 731d92a1b64342dfbf7c467af0a4c00e +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:1 of +msgid "TOFU check-and-record for a peer public key." +msgstr "" + +#: 634283c14fb44424bf148a34d156fb71 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:3 of +msgid "" +"``peer_pem`` is the PEM text received on this connection, ``(peer_ip, " +"peer_port)`` the endpoint it came from. Returns ``(ok, reason)``:" +msgstr "" + +#: 6bb58c88f8454d7daca92b5a478ae788 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:7 of +msgid "" +"key already registered under any endpoint -> accept, and re-register it " +"under the current endpoint when it moved (IPs are dynamic and ports are " +"user-changeable);" +msgstr "" + +#: 74e2bcc89bae4fe5aecf99e6208e21d1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:10 of +msgid "" +"key unknown but the endpoint already holds a *different* key -> reject (a" +" trusted endpoint suddenly presenting a new key);" +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:12 +#: f856b875adc34f4a80c2b21cd506b143 of +msgid "" +"key and endpoint both unknown -> accept and record (first connection is " +"trusted, TOFU)." +msgstr "" + +#: 556fdf46d6c543e0ab06e2d4fabad7f1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:1 of +msgid "Move a freshly received public key file into the key cache." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:3 +#: a47e84f5d21949bf98e24834f18bed76 of +msgid "" +"Idempotent under concurrency: several transfers may deliver the same peer" +" key at once (multi-connection handshakes, several client processes " +"sharing one ``received_files/`` directory); if the source is already gone" +" because a concurrent store moved it, success is assumed when the " +"destination is in place." +msgstr "" + +#: 135944d28da54948862e515eb828072d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:1 of +msgid "Encrypt ``plaintext`` with the peer's public key file." +msgstr "" + +#: 4b4dded0ad904ce2bb87c9a43c8f87e5 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:3 of +msgid "" +"Returns the ASCII wire body (no trailing newline): each chunk is RSA-OAEP" +" encrypted and base64 encoded, chunks joined with ``|``. Raises if no " +"peer key is stored at ``peer_pem_path`` yet. The whole encryption runs " +"under ``_peer_pub_cache_lock`` so the peer handle cannot be freed mid-" +"encrypt (no-GIL safe)." +msgstr "" + +#: 8f1687f4ab984c029859fd1f9cfb968c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:1 of +msgid "Decrypt a wire body with our private key." +msgstr "" + +#: 8a1d1504bf4248fa9e1288776d19810b +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:3 of +msgid "" +"Returns ``(True, plaintext)`` on success, or ``(False, None)`` when the " +"key is stale/wrong or the ``_VALID`` signature is missing. Runs under " +"``_key_lock`` so the handle cannot be freed by a concurrent " +"``reload_own_key`` (no-GIL safe)." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.po new file mode 100644 index 0000000..2444e70 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.po @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.rst:2 738ded08e11742acb4854652f885aa13 +msgid "PyFlow package" +msgstr "" + +#: ../../api/PyFlow.rst:10 8a3a28b0487c4bacb81f0afaa1b2902e +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.rst:19 28ca6a63167d49798f21d2796d6acf1e +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.po new file mode 100644 index 0000000..c691b29 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.po @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.rst:2 e8518825f60e48f391b84d3fb415bb36 +msgid "PyFlow.transfer\\_web package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:10 8d8e622c8bdd4618b0e33d94e1000e42 +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:19 541ef21ed94743f2b9e11aadbde918b4 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po new file mode 100644 index 0000000..3625739 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po @@ -0,0 +1,40 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_client.rst:2 +#: 09f60d1b11ac4e92b480fa6284489970 +msgid "PyFlow.transfer\\_web.setup\\_client module" +msgstr "" + +#: 2f786e174934474a93c6498878a68612 PyFlow.transfer_web.setup_client:1 of +msgid "PyFlow TCP client web launcher." +msgstr "" + +#: 3e6b6a90f3e7484f8a0ca766f8305082 PyFlow.transfer_web.setup_client:3 of +msgid "" +"Starts a lightweight Flask backend on 127.0.0.1 and opens the connect UI " +"in the browser. The user enters the server address (an http/https domain" +" or a bare IP); the backend asks the server's web backend for the TCP " +"server address/port, starts the TCP client, and keeps the backend running" +" to relay the user's frontend actions." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po new file mode 100644 index 0000000..fb6d9b6 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po @@ -0,0 +1,53 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_server.rst:2 +#: e6642bb55a204188b0abd0b07207713e +msgid "PyFlow.transfer\\_web.setup\\_server module" +msgstr "" + +#: 5966876a4564470ea674311ddb3ef63e PyFlow.transfer_web.setup_server:1 of +msgid "PyFlow TCP server web launcher." +msgstr "" + +#: 588fea0dc68a4a308f9a7cfb6fe5d819 PyFlow.transfer_web.setup_server:3 of +msgid "Checks ``transfer_web/.Flow_Web/setup_server.json``:" +msgstr "" + +#: PyFlow.transfer_web.setup_server:5 c3df4df524da40b18a58607efd9b5e4a of +msgid "" +"missing -> opens the server startup-configuration UI in the browser; the" +" UI saves the config (same shape as ``flow_setup``'s ``setup.json``) and " +"starts the TCP server class;" +msgstr "" + +#: 211850fce6d34d50ae152bce6d1aa3af PyFlow.transfer_web.setup_server:8 of +msgid "present -> starts the TCP server class directly from the saved config." +msgstr "" + +#: PyFlow.transfer_web.setup_server:10 cb510f0a77474eff8e8816fafc097343 of +msgid "" +"After the TCP server is up, the lightweight Flask backend serves the " +"status page and the client-facing API (``/api/server_info`` etc.) on the " +"server's address." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po new file mode 100644 index 0000000..052e5a3 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:2 +#: 76090548cb6a4066a3558553adad502a +msgid "PyFlow.transfer\\_web.web\\_backend package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:10 +#: 0d60b135791b45019583e72be4a02afc +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po new file mode 100644 index 0000000..6cc64b9 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po @@ -0,0 +1,120 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.server_backend.rst:2 +#: 4e16e69f9ae9491a95bfcab9c70cfe24 +msgid "PyFlow.transfer\\_web.web\\_backend.server\\_backend module" +msgstr "" + +#: 15eb6d4006b3429a8fa230a2b460b1c5 +#: PyFlow.transfer_web.web_backend.server_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP server for the web tool." +msgstr "" + +#: 3c413c9e37f94c7485dba6d4f96b9bf2 +#: PyFlow.transfer_web.web_backend.server_backend:3 of +msgid "Two modes, one process:" +msgstr "" + +#: 557b793ac8e441db86cf59cad5e85371 +#: PyFlow.transfer_web.web_backend.server_backend:5 of +msgid "" +"``config`` mode: serves the server startup-configuration UI. The UI " +"shows every ``TCP_Server_Base`` parameter with its default value; on " +"submit the config is written to ``.Flow_Web/setup_server.json`` (same " +"shape as ``flow_setup``'s ``setup.json``) and the TCP server class is " +"started." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:10 +#: cb922669795b42c6ad5587b527c4356a of +msgid "" +"``status`` mode: serves the minimal status page plus the same " +"sidebar/input UI as the client frontend (forwarding disabled; native " +"sends to connected clients allowed). Also exposes the HTTP API that " +"clients use to discover the TCP server address/port." +msgstr "" + +#: 0c6784517a194d65926c71b7ce7f4836 +#: PyFlow.transfer_web.web_backend.server_backend:15 of +msgid "" +"The backend monitors ``server.clients``: whenever a client connects or " +"disconnects it broadcasts the current instance list to every connected " +"client (``/web_clients_update``), and it re-checks the list every minute." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:20 +#: e902a63f173f4e8ba20923c4d8f4b083 of +msgid "" +"Inbound events (plain-text messages and file uploads arriving from " +"clients) are captured on the TCP server's receive threads through " +"``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` " +"APIs, queued here, and polled by the frontend via ``/api/events``." +msgstr "" + +#: 30582f045fae468cb63a543324edfea8 +#: PyFlow.transfer_web.web_backend.server_backend:25 of +msgid "" +"Authentication: anonymous visitors get a white landing page (the server " +"addresses plus a login button); the configuration and status pages need a" +" session. Accounts live in ``.Flow_Web/users.json``; the first run seeds" +" the ``admin``/``admin`` administrator, and the frontend warns on every " +"login until those default credentials are changed." +msgstr "" + +#: 5fbb587ae4814cd683d5940abf4af37b +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:1 of +msgid "Account store backing the server web login (``.Flow_Web/users.json``)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:3 +#: f16349131c524eba9d2fd07591592afb of +msgid "" +"Passwords are PBKDF2-SHA256 records with a per-user salt. A missing " +"store file seeds the default ``admin``/``admin`` administrator; a store " +"file that exists but cannot be read is *not* re-seeded, so a damaged file" +" can never silently restore the default account." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.authenticate:1 +#: cfd5d429421942b3b0576566b76575a8 of +#, python-brace-format +msgid "Return ``{\"username\", \"role\"}`` for valid credentials, else ``None``." +msgstr "" + +#: 56e3012561ed4ed4aa77ffea3a744f93 +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.change_credentials:1 +#: of +msgid "Rename ``username`` and set its password (self-service)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp:1 +#: ab4c37912c524489a26700248a06704c of +msgid "Flask app + TCP_Server_Base wrapper for the web tool." +msgstr "" + +#: 7e707a76895243cbb7e49d4a943df5f2 +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_server.json`` and start the TCP server." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po new file mode 100644 index 0000000..ece8b4f --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po @@ -0,0 +1,91 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.client_backend.rst:2 +#: a7e084a5059341aa9b7419059c4fa4b0 +msgid "PyFlow.transfer\\_web.web\\_front.client\\_backend module" +msgstr "" + +#: 22485674cb53445a86703fb118698523 +#: PyFlow.transfer_web.web_front.client_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP client for the web tool." +msgstr "" + +#: 31862d77d7184e55a1e729d7e72baed0 +#: PyFlow.transfer_web.web_front.client_backend:3 of +msgid "" +"The launcher (``setup_client.py``) starts this backend and opens the " +"connect UI in the browser. The user enters the server address (an " +"``http``/``https`` domain or a bare IP); the backend queries the server's" +" web backend ``/api/server_info`` for the TCP server address and port, " +"then starts the ``TCP_Client_Base`` instance. The backend stays up to " +"relay the user's frontend actions:" +msgstr "" + +#: 1a028258cda844ee945b9522d51afb9d +#: PyFlow.transfer_web.web_front.client_backend:10 of +msgid "messages/files/folders to the server use the native transfer methods;" +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:11 +#: e160c2a0daf9498cbe2041a8e964ff47 of +msgid "" +"messages to other clients use the native ``/forward_send_msg`` forwarding" +" (a client-only command relayed by the server);" +msgstr "" + +#: 16da18234f0a499893b711e7b555b42a +#: PyFlow.transfer_web.web_front.client_backend:13 of +msgid "" +"files/folders to other clients are forwarded through the built-in " +"``forward_extension_tcp`` extension." +msgstr "" + +#: 79dee862a03f4d0c9bc9403f8d461465 +#: PyFlow.transfer_web.web_front.client_backend:16 of +msgid "" +"The sidebar instance list is kept fresh by the server's " +"``/web_clients_update`` broadcasts; a reload button re-requests the list " +"via ``/web_sync_clients``." +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:20 +#: b952223fdafe4d52b8c34498aef1aeef of +msgid "" +"Inbound events (plain-text messages and files pushed by the server, " +"whether direct sends or client forwards) are captured on the TCP client's" +" receive threads through ``TCP_Client_Base``'s " +"``add_message_listener``/``add_file_listener`` APIs, queued here, and " +"polled by the frontend via ``/api/events``." +msgstr "" + +#: 0913e15917534119b775fb5c545c439d +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp:1 of +msgid "Flask app + TCP_Client_Base wrapper for the web tool." +msgstr "" + +#: 3e9a2f40d40b45869c4750ae3e542502 +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_client.json`` and start the TCP client." +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po new file mode 100644 index 0000000..4671118 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.rst:2 +#: 24ab9a394e604e0ea77050304ef77edd +msgid "PyFlow.transfer\\_web.web\\_front package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_front.rst:10 +#: ae412dcb5fd34608b462f44c2a8b9a17 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/ru/LC_MESSAGES/api/index.po b/docs/locale/ru/LC_MESSAGES/api/index.po new file mode 100644 index 0000000..373cf59 --- /dev/null +++ b/docs/locale/ru/LC_MESSAGES/api/index.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/index.rst:2 8d57b43b04b14ab8a07c05fb89785684 +msgid "API Reference" +msgstr "" + +#: ../../api/index.rst:4 2e33b7c5195747ebb1a15eb5e3e9c026 +msgid "" +"The pages below are generated from the code by ``sphinx-apidoc`` (see the" +" first line of ``docs/reBuild.sh``): each one pulls its text from the " +"docstrings at build time, so nothing here is written by hand." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/File_Transfer/File_Transfer.po b/docs/locale/zh_CN/LC_MESSAGES/File_Transfer/File_Transfer.po index fdf3be1..3e90f60 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/File_Transfer/File_Transfer.po +++ b/docs/locale/zh_CN/LC_MESSAGES/File_Transfer/File_Transfer.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 13:19+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: zh_CN \n" @@ -644,11 +644,17 @@ msgid "Commands (client console only; rejected on the server console):" msgstr "客户端控制台(接收者是服务器):" #: ../../File_Transfer/File_Transfer.rst:413 14406316555743359a854540ab6937c8 -msgid "``/forward_file ... ...``" +#, fuzzy +msgid "" +"``/forward_file ... ... " +"[destination_file_path]``" msgstr "``/forward_file <文件1> <文件2> ... <地址1> <地址2> ...``" #: ../../File_Transfer/File_Transfer.rst:414 4dc576898177404da124649f7ce9ac28 -msgid "``/forward_folder ... ...``" +#, fuzzy +msgid "" +"``/forward_folder ... ... " +"[destination_file_path]``" msgstr "``/forward_folder <文件夹1> <文件夹2> ... <地址1> <地址2> ...``" #: ../../File_Transfer/File_Transfer.rst:416 220dfbf0a0d84559a8befb4eee500ba0 @@ -660,17 +666,31 @@ msgid "" msgstr "" "文件/文件夹的数量和目标客户端的数量(写为带引号的地址元组)是无限的。无法到达(未连接到服务器)或等于服务器本身的目标地址将被跳过,而其余目标仍将得到服务。" -#: ../../File_Transfer/File_Transfer.rst:424 f6697695e9ef4e3694fa9046e41578e0 +#: ../../File_Transfer/File_Transfer.rst:424 6137bf9406784c6f82d9f41560a52566 +msgid "" +"Like every transfer family, both commands accept an optional trailing " +"``destination_file_path`` that replaces the default save directory on every " +"receiving client: a forwarded file lands at ``/`` and" +" a forwarded folder keeps its structure under " +"``//...``. When the argument is omitted the " +"targets write to their default ``file_transfer_dir``." +msgstr "" +"像每个传输家族一样,这两个命令都接受一个可选的后缀`` destination_file_path '' " +",替换每个接收客户端上的默认保存目录:转发文件位于``/`` " +",转发文件夹将其结构保留在``//... ``下。当参数被省略时,目标写入其默认`` " +"file_transfer_dir ``。" + +#: ../../File_Transfer/File_Transfer.rst:435 f6697695e9ef4e3694fa9046e41578e0 msgid "The data path reuses the protocol's own transfer machinery:" msgstr "数据路径重用协议自己的传输机制:" -#: ../../File_Transfer/File_Transfer.rst:427 6ee037f0e4024d2988fcb2ccf3deef70 +#: ../../File_Transfer/File_Transfer.rst:438 6ee037f0e4024d2988fcb2ccf3deef70 msgid "" "The forwarding client streams the file with the standard file-transfer byte " "stream (metadata header + 64 KiB chunks) to a transfer socket on the server." msgstr "转发客户端将带有标准文件传输字节流(元数据标头 + 64 KiB 块)的文件流式传输到服务器上的传输套接字。" -#: ../../File_Transfer/File_Transfer.rst:432 43c94e746a5c46748846d0dc18a4e0ff +#: ../../File_Transfer/File_Transfer.rst:443 43c94e746a5c46748846d0dc18a4e0ff msgid "" "The server acts as a pure relay: it reads the stream into per-target memory " "queues and writes each chunk to every target's transfer socket. The server " @@ -679,7 +699,7 @@ msgid "" msgstr "" "服务器充当纯粹的中继:它将流读取到每个目标的内存队列中,并将每个块写入每个目标的传输套接字。服务器从不解析超出大小标头的文件内容,也从不写入磁盘。" -#: ../../File_Transfer/File_Transfer.rst:439 9dec57916af34e109bee7ce1e0bae2d4 +#: ../../File_Transfer/File_Transfer.rst:450 9dec57916af34e109bee7ce1e0bae2d4 msgid "" "Every target client receives the stream with the ordinary receive path " "(``file_transfer_mode_recv``) and writes it to its own local disk, exactly " @@ -687,11 +707,11 @@ msgid "" msgstr "" "每个目标客户端都使用普通接收路径(“file_transfer_mode_recv”)接收流并将其写入自己的本地磁盘,就像服务器直接推送文件一样。" -#: ../../File_Transfer/File_Transfer.rst:445 a095abaf821e41a6a66adbb776bc35ef +#: ../../File_Transfer/File_Transfer.rst:456 a095abaf821e41a6a66adbb776bc35ef msgid "### Memory Bounding and Flow Control" msgstr "### 内存限制和流量控制" -#: ../../File_Transfer/File_Transfer.rst:447 244214dd675345f8a72b1088ad99fea0 +#: ../../File_Transfer/File_Transfer.rst:458 244214dd675345f8a72b1088ad99fea0 msgid "" "Because uploader, server and targets may have different bandwidths, data can" " pile up in the server's memory. Both ``TCP_Server_Base`` and " @@ -710,25 +730,25 @@ msgstr "" "都采用 max_mem_buff 参数(以 MB 为单位,默认 2048,即 2 " "GB),该参数限制了转发机制在该进程中可以保留的内存。当服务器的缓冲字节超过“max_mem_buff”时,它会向转发客户端发送“/pause_trans”,从而停止读取源文件;一旦作者将缓冲区排空到低水位线(限制的一半)以下,服务器就会发送“/start_trans”并恢复上传。接收客户端将每个块同步排出到磁盘,因此它们的缓冲内存保持受单个块的限制;两侧都存在“/pause_trans”/“/start_trans”处理程序,因此任何一侧都可以在缓冲数据时限制传输。" -#: ../../File_Transfer/File_Transfer.rst:472 0aa14deaf6474437b421a65e21fce7d8 +#: ../../File_Transfer/File_Transfer.rst:483 0aa14deaf6474437b421a65e21fce7d8 msgid "Concurrency and Threading" msgstr "并发和线程" -#: ../../File_Transfer/File_Transfer.rst:474 825c5e1a8b9b405c967ca49a1861e258 +#: ../../File_Transfer/File_Transfer.rst:485 825c5e1a8b9b405c967ca49a1861e258 msgid "" "Both the server and the client use multiple levels of concurrency control to" " ensure stability during file transfers." msgstr "服务器和客户端均采用多级并发控制来保证文件传输过程中的稳定性。" -#: ../../File_Transfer/File_Transfer.rst:478 0d48b1245cd54597928cfc28d5b3c248 +#: ../../File_Transfer/File_Transfer.rst:489 0d48b1245cd54597928cfc28d5b3c248 msgid "### File Transfer Semaphore" msgstr "### 文件传输信号量" -#: ../../File_Transfer/File_Transfer.rst:480 e2674ee9e0584513bc53aa815603fd24 +#: ../../File_Transfer/File_Transfer.rst:491 e2674ee9e0584513bc53aa815603fd24 msgid "Client: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" msgstr "客户端:``self.file_semaphore = threading.Semaphore(max_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:482 1c0d27d13ad844159d29c6e662d51d09 +#: ../../File_Transfer/File_Transfer.rst:493 1c0d27d13ad844159d29c6e662d51d09 msgid "" "Server: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" @@ -736,18 +756,18 @@ msgstr "" "服务器:``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:485 5b0c8289b2c9460cb7305101226b66a7 +#: ../../File_Transfer/File_Transfer.rst:496 5b0c8289b2c9460cb7305101226b66a7 msgid "" "This semaphore limits the number of simultaneous file transfers (used " "primarily when sending folders or multiple files). Each transfer runs in its" " own thread, and the semaphore is acquired before the thread is started." msgstr "此信号量限制同时文件传输的数量(主要在发送文件夹或多个文件时使用)。每个传输都在自己的线程中运行,并且在线程启动之前获取信号量。" -#: ../../File_Transfer/File_Transfer.rst:492 6d93783b298e4ce98b999ff890d4a5f4 +#: ../../File_Transfer/File_Transfer.rst:503 6d93783b298e4ce98b999ff890d4a5f4 msgid "### Threading Model" msgstr "### 线程模型" -#: ../../File_Transfer/File_Transfer.rst:494 036a972600e645119d8a523fa08a52db +#: ../../File_Transfer/File_Transfer.rst:505 036a972600e645119d8a523fa08a52db msgid "" "Each file transfer runs in a dedicated daemon thread, created by the " "``_thread`` wrapper functions (e.g., " @@ -756,14 +776,14 @@ msgid "" msgstr "" "每个文件传输都在一个专用的守护线程中运行,该守护线程由“_thread”包装函数创建(例如“file_transfer_client_recv_client_start_thread”)。这可以防止缓慢的传输阻塞主控制循环。" -#: ../../File_Transfer/File_Transfer.rst:500 a84aaf5f9fb64d92b97c32185a1e7cb5 +#: ../../File_Transfer/File_Transfer.rst:511 a84aaf5f9fb64d92b97c32185a1e7cb5 msgid "" "The thread that receives the transfer command (e.g., the server's " "``handle_command`` thread) does not wait for the transfer to complete; it " "returns immediately after spawning the worker thread." msgstr "接收传输命令的线程(例如服务器的“handle_command”线程)不会等待传输完成;它在产生工作线程后立即返回。" -#: ../../File_Transfer/File_Transfer.rst:505 669fb6da8e7444dd829a6e14295648b1 +#: ../../File_Transfer/File_Transfer.rst:516 669fb6da8e7444dd829a6e14295648b1 msgid "" "The low-level receive function (``file_transfer_mode_recv``) blocks while " "reading from the transfer socket, but because it runs in a dedicated thread," @@ -771,11 +791,11 @@ msgid "" msgstr "" "低级接收函数(“file_transfer_mode_recv”)在从传输套接字读取时会阻塞,但由于它在专用线程中运行,因此主连接仍保持响应。" -#: ../../File_Transfer/File_Transfer.rst:511 e39f8ae06ae84a6b84de8a3008ca17cb +#: ../../File_Transfer/File_Transfer.rst:522 e39f8ae06ae84a6b84de8a3008ca17cb msgid "### Thread Pool for Custom Commands" msgstr "### 自定义命令的线程池" -#: ../../File_Transfer/File_Transfer.rst:513 3ad49853529d4563bacdfb4ca6eee115 +#: ../../File_Transfer/File_Transfer.rst:524 3ad49853529d4563bacdfb4ca6eee115 msgid "" "Both classes also provide a ``ThreadPoolExecutor`` " "(``self._custom_executor``) for custom command handlers. When a handler is " @@ -786,11 +806,11 @@ msgid "" msgstr "" "这两个类还为自定义命令处理程序提供了“ThreadPoolExecutor”(“self._custom_executor”)。当使用“run_in_thread=True”注册处理程序时,它会通过“submit_task”提交到该池,该池还使用信号量将并发限制为“max_custom_workers”。此机制独立于文件传输信号量,旨在用于通用命令处理。" -#: ../../File_Transfer/File_Transfer.rst:528 1862d74da28a40219f82cfd7af89c126 +#: ../../File_Transfer/File_Transfer.rst:539 1862d74da28a40219f82cfd7af89c126 msgid "Port Allocation and Management" msgstr "端口分配与管理" -#: ../../File_Transfer/File_Transfer.rst:530 bd53ec8aa4ef498aa0815db16ed03140 +#: ../../File_Transfer/File_Transfer.rst:541 bd53ec8aa4ef498aa0815db16ed03140 msgid "" "File transfers require ephemeral ports for the secondary data connections. " "The ``palloc()`` and ``pfree()`` methods are used to obtain and release " @@ -798,7 +818,7 @@ msgid "" msgstr "" "文件传输需要临时端口来进行辅助数据连接。 ``palloc()`` 和 ``pfree()`` 方法用于获取和释放这些端口。有两种模式可供选择:" -#: ../../File_Transfer/File_Transfer.rst:536 eeaa9379d7d941159ac34f30ff6bb5f9 +#: ../../File_Transfer/File_Transfer.rst:547 eeaa9379d7d941159ac34f30ff6bb5f9 msgid "" "**Automatic mode** (``is_hand_alloc_port=False``): ``palloc()`` returns " "``0``, and the operating system assigns a free port when the socket is " @@ -807,7 +827,7 @@ msgstr "" "**自动模式**(``is_hand_alloc_port=False``):``palloc()`` " "返回``0``,操作系统在套接字绑定时分配一个空闲端口。这是大多数用例的推荐模式。" -#: ../../File_Transfer/File_Transfer.rst:541 80e08ac6614b4dcd88cd0e4d31a3da64 +#: ../../File_Transfer/File_Transfer.rst:552 80e08ac6614b4dcd88cd0e4d31a3da64 msgid "" "**Manual mode** (``is_hand_alloc_port=True``): Ports are drawn from a " "configurable range ``[self.min_port, self.max_port]`` with a step size " @@ -819,7 +839,7 @@ msgstr "" "self.max_port]`` " "中提取端口,步长为``port_add_step``。服务器通过“/client_alloc_port_range”向客户端广播允许的范围,然后客户端使用相同的手动分配逻辑。" -#: ../../File_Transfer/File_Transfer.rst:551 29cfdab183eb4f9f83de41ef62a60785 +#: ../../File_Transfer/File_Transfer.rst:562 29cfdab183eb4f9f83de41ef62a60785 msgid "" "*Note: For more details about port allocation, please visit the Port " "Allocation API sections in :doc:`TCP_Server_APIs` and " @@ -827,61 +847,61 @@ msgid "" msgstr "" "*注意:有关端口分配的更多详细信息,请访问 TCP_Server_APIs 和 TCP_Client_APIs 中的端口分配 API 部分。*" -#: ../../File_Transfer/File_Transfer.rst:559 189736d8d1f8464b8d301f219700fda4 +#: ../../File_Transfer/File_Transfer.rst:570 189736d8d1f8464b8d301f219700fda4 msgid "Error Handling and Timeouts" msgstr "错误处理和超时" -#: ../../File_Transfer/File_Transfer.rst:561 bcc373777989436f9df3657446d1a7f2 +#: ../../File_Transfer/File_Transfer.rst:572 bcc373777989436f9df3657446d1a7f2 msgid "### Timeout Values" msgstr "### 超时值" -#: ../../File_Transfer/File_Transfer.rst:563 27d75d097a804ec28ebec5c67469c9c6 +#: ../../File_Transfer/File_Transfer.rst:574 27d75d097a804ec28ebec5c67469c9c6 msgid "" "**Start signal timeout**: 10 seconds. If the receiver does not send " "``server_start_file_transfer_sign`` within this time, the sender aborts." msgstr "" "**启动信号超时**:10 秒。如果接收方在此时间内未发送“server_start_file_transfer_sign”,则发送方将中止。" -#: ../../File_Transfer/File_Transfer.rst:567 93e877c411eb4c96bd7861ff61f35e7a +#: ../../File_Transfer/File_Transfer.rst:578 93e877c411eb4c96bd7861ff61f35e7a msgid "" "**Port negotiation timeout**: 20 seconds. The initiator waits for the peer's" " ``/server_file_transfer_port`` response." msgstr "**端口协商超时**:20 秒。发起者等待对等方的“/server_file_transfer_port”响应。" -#: ../../File_Transfer/File_Transfer.rst:570 9e1d395bb59540a49393bebe6630e5eb +#: ../../File_Transfer/File_Transfer.rst:581 9e1d395bb59540a49393bebe6630e5eb msgid "" "**Completion acknowledgement timeout**: ``30 + (file_size // (100 * 1024 * " "1024)) * 10`` seconds. Larger files get proportionally more time." msgstr "" "**完成确认超时**:``30 + (file_size // (100 * 1024 * 1024)) * 10`` 秒。文件越大,相应的时间就越长。" -#: ../../File_Transfer/File_Transfer.rst:574 6b4e6135669b41178fdbb47ea5975381 +#: ../../File_Transfer/File_Transfer.rst:585 6b4e6135669b41178fdbb47ea5975381 msgid "### Error Signalling" msgstr "### 错误信号" -#: ../../File_Transfer/File_Transfer.rst:576 448fdc6ba3dd4d9eae925e7e241f8cb8 +#: ../../File_Transfer/File_Transfer.rst:587 448fdc6ba3dd4d9eae925e7e241f8cb8 msgid "" "Any error during the handshake or data transfer causes the failing side to " "send ``error_sign`` over the transfer socket." msgstr "握手或数据传输期间的任何错误都会导致失败方通过传输套接字发送“error_sign”。" -#: ../../File_Transfer/File_Transfer.rst:579 5b4c23a93e984ce0af2e43cf4752225b +#: ../../File_Transfer/File_Transfer.rst:590 5b4c23a93e984ce0af2e43cf4752225b msgid "" "The other side, upon receiving the error sign, closes the transfer socket " "and aborts the transfer." msgstr "另一端收到错误标志后,关闭传输套接字并中止传输。" -#: ../../File_Transfer/File_Transfer.rst:582 b337a2726ded4d619c5e8026bef3f6ea +#: ../../File_Transfer/File_Transfer.rst:593 b337a2726ded4d619c5e8026bef3f6ea msgid "" "The main control connection remains unaffected; only the transfer socket is " "closed." msgstr "主控连接不受影响;仅关闭传输套接字。" -#: ../../File_Transfer/File_Transfer.rst:586 68be52c8e5ce447a9c5ec51a661229cf +#: ../../File_Transfer/File_Transfer.rst:597 68be52c8e5ce447a9c5ec51a661229cf msgid "### Exception Handling" msgstr "### 异常处理" -#: ../../File_Transfer/File_Transfer.rst:588 036db9bc1858417ca589816028b83f80 +#: ../../File_Transfer/File_Transfer.rst:599 036db9bc1858417ca589816028b83f80 msgid "" "All socket operations are wrapped in try-except blocks. When an exception " "occurs (e.g., connection reset, file not found), the error is logged with " @@ -892,11 +912,11 @@ msgstr "" "块中。当发生异常时(例如,连接重置、未找到文件),错误将用``traceback.print_exc()`` " "记录,并且传输会正常中止。如果可能,将发送“error_sign”,并关闭传输套接字。" -#: ../../File_Transfer/File_Transfer.rst:600 fe9377db9cf04b9da0dbe4d07c730adf +#: ../../File_Transfer/File_Transfer.rst:611 fe9377db9cf04b9da0dbe4d07c730adf msgid "Related API Definitions" msgstr "相关API定义" -#: ../../File_Transfer/File_Transfer.rst:602 cc71cb9b1d6642a2acc89d45a49022cc +#: ../../File_Transfer/File_Transfer.rst:613 cc71cb9b1d6642a2acc89d45a49022cc msgid "" "This section lists all public file-transfer related methods in " "``TCP_Server_Base`` and ``TCP_Client_Base``. For a complete list of all " @@ -905,11 +925,11 @@ msgstr "" "本节列出了“TCP_Server_Base”和“TCP_Client_Base”中所有与公共文件传输相关的方法。有关所有公共 API " "的完整列表,请参阅本文档末尾的表格。" -#: ../../File_Transfer/File_Transfer.rst:608 d6683e415f794c5bb693f8c24370e7f9 +#: ../../File_Transfer/File_Transfer.rst:619 d6683e415f794c5bb693f8c24370e7f9 msgid "### Server-Side File Transfer APIs" msgstr "### 服务器端文件传输 API" -#: ../../File_Transfer/File_Transfer.rst:618 9123c69197c34d93bb68d088897ebeca +#: ../../File_Transfer/File_Transfer.rst:629 9123c69197c34d93bb68d088897ebeca msgid "" "Initiates a server-to-client file transfer. ``message`` is the command " "string (e.g., ``/file /path/to/file.txt (127.0.0.1,54321)``). If " @@ -919,11 +939,11 @@ msgstr "" "启动服务器到客户端的文件传输。 ``message`` 是命令字符串(例如,``/file /path/to/file.txt " "(127.0.0.1,54321)``)。如果提供了“file_folder_abspath”(用于文件夹传输),则它指定父文件夹的绝对路径。" -#: ../../File_Transfer/File_Transfer.rst:633 367dfd82c95c413d963a15152469fc44 +#: ../../File_Transfer/File_Transfer.rst:644 367dfd82c95c413d963a15152469fc44 msgid "Thread-safe version that starts a new thread for the transfer." msgstr "线程安全版本,启动新线程进行传输。" -#: ../../File_Transfer/File_Transfer.rst:642 d4aa76b7eb2c46c29adee0120a939b66 +#: ../../File_Transfer/File_Transfer.rst:653 d4aa76b7eb2c46c29adee0120a939b66 msgid "" "Sends an entire folder from server to client. ``message`` should be of the " "form ``/file_folder ``." @@ -931,7 +951,7 @@ msgstr "" "将整个文件夹从服务器发送到客户端。 “message” 的格式应为“/file_folder " "”。" -#: ../../File_Transfer/File_Transfer.rst:652 fcb785ff144746fab81e95ec2ab056e1 +#: ../../File_Transfer/File_Transfer.rst:663 fcb785ff144746fab81e95ec2ab056e1 msgid "" "Sends multiple files to multiple clients. The message format is " "``/multiple_file_multiple_client ... " @@ -940,7 +960,7 @@ msgstr "" "将多个文件发送给多个客户端。消息格式为``/multiple_file_multiple_client ... " " ...``。文件必须出现在客户面前。" -#: ../../File_Transfer/File_Transfer.rst:664 9c6f024528f344e399b62023c7f8c858 +#: ../../File_Transfer/File_Transfer.rst:675 9c6f024528f344e399b62023c7f8c858 msgid "" "Sends different file lists to different clients. The message alternates " "between groups: a list of files, then a list of client addresses, then the " @@ -950,107 +970,107 @@ msgstr "" "向不同的客户端发送不同的文件列表。消息在组之间交替:文件列表,然后是客户端地址列表,然后是下一个文件列表,等等。示例:``/diff_multiple_file_diff_multiple_client" " a.txt b.txt (ip1,port1) (ip2,port2) c.txt (ip3,port3)``" -#: ../../File_Transfer/File_Transfer.rst:682 f8a880287b7b49d4bdc2239ecf4a0577 +#: ../../File_Transfer/File_Transfer.rst:693 f8a880287b7b49d4bdc2239ecf4a0577 msgid "" "Receives a file from a client. Called internally when the server receives a " "``/file`` command from a client." msgstr "从客户端接收文件。当服务器从客户端接收到“/file”命令时在内部调用。" -#: ../../File_Transfer/File_Transfer.rst:698 97bfaa9c24ae47c39328707b8f17a91a +#: ../../File_Transfer/File_Transfer.rst:709 97bfaa9c24ae47c39328707b8f17a91a msgid "" "Low-level receive function that performs the handshake and writes the " "incoming file to disk." msgstr "执行握手并将传入文件写入磁盘的低级接收函数。" -#: ../../File_Transfer/File_Transfer.rst:711 db6a97ca42ba43e59d5c20695039d4ee +#: ../../File_Transfer/File_Transfer.rst:722 db6a97ca42ba43e59d5c20695039d4ee msgid "" "Low-level send function that connects to the receiver and transmits the " "file." msgstr "连接到接收器并传输文件的低级发送函数。" -#: ../../File_Transfer/File_Transfer.rst:713 3e7b689215e840bebd368b4d29104ebc +#: ../../File_Transfer/File_Transfer.rst:724 3e7b689215e840bebd368b4d29104ebc msgid "### Client-Side File Transfer APIs" msgstr "### 客户端文件传输 API" -#: ../../File_Transfer/File_Transfer.rst:723 1e97e492d6504579a9a265eb1242395e +#: ../../File_Transfer/File_Transfer.rst:734 1e97e492d6504579a9a265eb1242395e msgid "" "Initiates a client-to-server file transfer. ``message`` is the user command " "(e.g., ``/file mydoc.txt``). Used internally by the interactive console." msgstr "启动客户端到服务器的文件传输。 “message” 是用户命令(例如“/file mydoc.txt”)。由交互式控制台内部使用。" -#: ../../File_Transfer/File_Transfer.rst:735 -#: ../../File_Transfer/File_Transfer.rst:786 0cd2695763114a0b831df0bfa80a3d56 +#: ../../File_Transfer/File_Transfer.rst:746 +#: ../../File_Transfer/File_Transfer.rst:797 0cd2695763114a0b831df0bfa80a3d56 msgid "Thread-safe version." msgstr "线程安全版本。" -#: ../../File_Transfer/File_Transfer.rst:744 261399ca508d463eafa7f03a00bfc658 +#: ../../File_Transfer/File_Transfer.rst:755 261399ca508d463eafa7f03a00bfc658 msgid "Sends a folder from client to server." msgstr "将文件夹从客户端发送到服务器。" -#: ../../File_Transfer/File_Transfer.rst:753 ef5e3b92b11c4530960c1c344a51c73b +#: ../../File_Transfer/File_Transfer.rst:764 ef5e3b92b11c4530960c1c344a51c73b msgid "Sends multiple files from client to server." msgstr "将多个文件从客户端发送到服务器。" -#: ../../File_Transfer/File_Transfer.rst:762 f2c90ee949d7484480cbb2cd5310bf26 +#: ../../File_Transfer/File_Transfer.rst:773 f2c90ee949d7484480cbb2cd5310bf26 msgid "Sends multiple folders from client to server." msgstr "将多个文件夹从客户端发送到服务器。" -#: ../../File_Transfer/File_Transfer.rst:775 0b13d26a40174243a15698b4bfcbb69f +#: ../../File_Transfer/File_Transfer.rst:786 0b13d26a40174243a15698b4bfcbb69f msgid "" "Receives a file from the server (called when the server initiates a " "transfer)." msgstr "从服务器接收文件(在服务器启动传输时调用)。" -#: ../../File_Transfer/File_Transfer.rst:797 3b78a4355d7f4ee2bbbe6bf934a962c0 +#: ../../File_Transfer/File_Transfer.rst:808 3b78a4355d7f4ee2bbbe6bf934a962c0 msgid "Receives a folder from the server." msgstr "从服务器接收文件夹。" -#: ../../File_Transfer/File_Transfer.rst:812 3d044e0754b94d1289b491502ce83610 +#: ../../File_Transfer/File_Transfer.rst:823 3d044e0754b94d1289b491502ce83610 msgid "Low-level receive function on the client side." msgstr "客户端的低级接收函数。" -#: ../../File_Transfer/File_Transfer.rst:824 7311023a7fa644ed9b57a2873cd3bca8 +#: ../../File_Transfer/File_Transfer.rst:835 7311023a7fa644ed9b57a2873cd3bca8 msgid "" "Low‑level send function on the client side (identical to server's version)." msgstr "客户端的低级发送功能(与服务器版本相同)。" -#: ../../File_Transfer/File_Transfer.rst:829 7e74e9a09a8a4cf2a8a372a50b1ee51b +#: ../../File_Transfer/File_Transfer.rst:840 7e74e9a09a8a4cf2a8a372a50b1ee51b msgid "Public API Summary" msgstr "公共API摘要" -#: ../../File_Transfer/File_Transfer.rst:831 87aaf0a5d3b44f41a419d97b6567f6d0 +#: ../../File_Transfer/File_Transfer.rst:842 87aaf0a5d3b44f41a419d97b6567f6d0 msgid "" "All public APIs (including non-file-transfer methods) are listed below for " "reference." msgstr "下面列出了所有公共 API(包括非文件传输方法)以供参考。" -#: ../../File_Transfer/File_Transfer.rst:835 a195ee393f2a442c810e59811a6ae126 +#: ../../File_Transfer/File_Transfer.rst:846 a195ee393f2a442c810e59811a6ae126 msgid "### TCP_Server_Base Public APIs" msgstr "### TCP_Server_Base 公共 API" -#: ../../File_Transfer/File_Transfer.rst:837 0408d74a9140472e9a774143a60e5749 +#: ../../File_Transfer/File_Transfer.rst:848 0408d74a9140472e9a774143a60e5749 msgid "``file_transfer_server_recv_client_start``" msgstr "``file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:838 24a33cb212844e04a8a271ada32412f0 +#: ../../File_Transfer/File_Transfer.rst:849 24a33cb212844e04a8a271ada32412f0 msgid "``file_transfer_server_recv_client_start_thread``" msgstr "``file_transfer_server_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:839 7e286d1ece6940868bea6b937486e617 +#: ../../File_Transfer/File_Transfer.rst:850 7e286d1ece6940868bea6b937486e617 msgid "``folder_file_transfer_server_recv_client_start``" msgstr "``folder_file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:840 37fc5bd346c84415bb15138a42508fbe +#: ../../File_Transfer/File_Transfer.rst:851 37fc5bd346c84415bb15138a42508fbe msgid "``multiple_file_multiple_client_transfer_server_recv_client_start``" msgstr "``multiple_file_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:841 38a2b16e3ab648688cf7e8d1cae8be72 +#: ../../File_Transfer/File_Transfer.rst:852 38a2b16e3ab648688cf7e8d1cae8be72 msgid "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" msgstr "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:843 11476e0eaa3b4a4286612812e4e2c004 +#: ../../File_Transfer/File_Transfer.rst:854 11476e0eaa3b4a4286612812e4e2c004 msgid "" "(The low-level helpers ``file_transfer_server_recv_server_start``, " "``file_transfer_mode_recv``, and ``file_transfer_mode`` are not considered " @@ -1058,65 +1078,65 @@ msgid "" msgstr "" "(低级助手“file_transfer_server_recv_server_start”、“file_transfer_mode_recv”和“file_transfer_mode”不被认为是公共的,但为了完整性而被记录下来。)" -#: ../../File_Transfer/File_Transfer.rst:849 907e861cebe648fbacb799bae8bb15e0 +#: ../../File_Transfer/File_Transfer.rst:860 907e861cebe648fbacb799bae8bb15e0 msgid "### TCP_Client_Base Public APIs" msgstr "### TCP_Client_Base 公共 API" -#: ../../File_Transfer/File_Transfer.rst:851 88bd80083f754caeb026f9ce1b8c6b55 +#: ../../File_Transfer/File_Transfer.rst:862 88bd80083f754caeb026f9ce1b8c6b55 msgid "``file_transfer_client_recv_client_start``" msgstr "``file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:852 3b21ff73da694465906482412b4fb4e3 +#: ../../File_Transfer/File_Transfer.rst:863 3b21ff73da694465906482412b4fb4e3 msgid "``file_transfer_client_recv_client_start_thread``" msgstr "``file_transfer_client_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:853 c8af43896a2443a5b14bd29863731c2c +#: ../../File_Transfer/File_Transfer.rst:864 c8af43896a2443a5b14bd29863731c2c msgid "``folder_file_transfer_client_recv_client_start``" msgstr "``folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:854 7a3d3e4a52334b169a62d3b30d7a3190 +#: ../../File_Transfer/File_Transfer.rst:865 7a3d3e4a52334b169a62d3b30d7a3190 msgid "``multiple_file_transfer_client_recv_client_start``" msgstr "``multiple_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:855 19e18754c4674842ad5116f709588e03 +#: ../../File_Transfer/File_Transfer.rst:866 19e18754c4674842ad5116f709588e03 msgid "``multiple_folder_file_transfer_client_recv_client_start``" msgstr "``multiple_folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:856 4cf80f9f34e045c59a680d0450c9103a +#: ../../File_Transfer/File_Transfer.rst:867 4cf80f9f34e045c59a680d0450c9103a msgid "``file_transfer_client_recv_server_start``" msgstr "``file_transfer_client_recv_server_start``" -#: ../../File_Transfer/File_Transfer.rst:857 c09f5fcba03f461f89238dd31abf6e88 +#: ../../File_Transfer/File_Transfer.rst:868 c09f5fcba03f461f89238dd31abf6e88 msgid "``file_transfer_client_recv_server_start_thread``" msgstr "``file_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:858 ad39bc6b71f048b09bca3b895e9d32f8 +#: ../../File_Transfer/File_Transfer.rst:869 ad39bc6b71f048b09bca3b895e9d32f8 msgid "``file_folder_transfer_client_recv_server_start_thread``" msgstr "``file_folder_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:860 99140d0be72649199911a57a26e2f2cf +#: ../../File_Transfer/File_Transfer.rst:871 99140d0be72649199911a57a26e2f2cf msgid "(The low-level helpers are documented but not part of the public API.)" msgstr "(低级帮助程序已记录,但不属于公共 API 的一部分。)" -#: ../../File_Transfer/File_Transfer.rst:864 60355543ac904504af8529431ce2c1fa +#: ../../File_Transfer/File_Transfer.rst:875 60355543ac904504af8529431ce2c1fa msgid "See Also" msgstr "参见" -#: ../../File_Transfer/File_Transfer.rst:866 7a5902ad3de64bf79d54d7f2f83ecbfb +#: ../../File_Transfer/File_Transfer.rst:877 7a5902ad3de64bf79d54d7f2f83ecbfb msgid "" "For more information about the TCP server and client base classes, please " "refer to:" msgstr "关于TCP服务器和客户端基类的更多信息,请参考:" -#: ../../File_Transfer/File_Transfer.rst:870 f10c29f467b849e8b4254998b44f99ba +#: ../../File_Transfer/File_Transfer.rst:881 f10c29f467b849e8b4254998b44f99ba msgid ":doc:`../Network_APIs/TCP_Server_APIs`" msgstr ":doc:`../Network_APIs/TCP_Server_APIs`" -#: ../../File_Transfer/File_Transfer.rst:871 335ba244d28342449db065c252d7e14c +#: ../../File_Transfer/File_Transfer.rst:882 335ba244d28342449db065c252d7e14c msgid ":doc:`../Network_APIs/TCP_Client_APIs`" msgstr ":doc:`../Network_APIs/TCP_Client_APIs`" -#: ../../File_Transfer/File_Transfer.rst:873 92d0227c356447a098cba072d5b43c98 +#: ../../File_Transfer/File_Transfer.rst:884 92d0227c356447a098cba072d5b43c98 msgid "" "For details on port allocation, see the Port Allocation API sections in " "those documents." diff --git a/docs/locale/zh_CN/LC_MESSAGES/Instance_Setup/Instance_Setup.po b/docs/locale/zh_CN/LC_MESSAGES/Instance_Setup/Instance_Setup.po index 8e5f461..004a15d 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Instance_Setup/Instance_Setup.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Instance_Setup/Instance_Setup.po @@ -8,20 +8,20 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-30 23:45+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.18.0\n" -#: ../../Instance_Setup/Instance_Setup.rst:3 707444a8cdb246fd81a189c038518c82 +#: ../../Instance_Setup/Instance_Setup.rst:3 552b0fbec3774a638b0029ce5fd72949 msgid "Flow Setup Launcher" msgstr "流程设置启动器" -#: ../../Instance_Setup/Instance_Setup.rst:5 a611d1bbc1bc4b12a9db0997909626bc +#: ../../Instance_Setup/Instance_Setup.rst:5 31952c3324264ea28368c7725796ff63 msgid "" "The ``flow_setup.py`` script is a launcher for the TCP server/client " "framework defined in ``connect_tcp.py``. It allows you to quickly spawn a " @@ -32,7 +32,7 @@ msgstr "" "“flow_setup.py” 脚本是“connect_tcp.py” 中定义的 TCP " "服务器/客户端框架的启动器。它允许您以交互方式或通过命令行参数快速生成单个服务器或客户端实例。每个启动的实例都在单独的终端窗口(或无头系统上的后台进程)中运行。" -#: ../../Instance_Setup/Instance_Setup.rst:12 bc0bbe4590e04437827678fe85f482fb +#: ../../Instance_Setup/Instance_Setup.rst:12 4f99dd7362fa4dcb8f83de37f327dbef msgid "" "**Note:** This launcher supports only **one server** and **one client** " "instance at a time. Adding a new server or client configuration will " @@ -40,20 +40,20 @@ msgid "" msgstr "" "**注意:** 此启动器一次仅支持 **一个服务器** 和 **一个客户端** 实例。添加新的服务器或客户端配置将完全覆盖任何先前的相同类型的配置。" -#: ../../Instance_Setup/Instance_Setup.rst:18 146d87f809054469be52a0d4174907fc +#: ../../Instance_Setup/Instance_Setup.rst:18 c8f9652279de42bb983523a15048596e msgid "Features" msgstr "特征" -#: ../../Instance_Setup/Instance_Setup.rst:20 ea5ca94177b349b3852287e9327f792e +#: ../../Instance_Setup/Instance_Setup.rst:20 fba5b9ea870c44fabf69022a45cdaebc msgid "" "**Interactive mode** – step‑by‑step creation of a server or client instance." msgstr "**交互模式** – 逐步创建服务器或客户端实例。" -#: ../../Instance_Setup/Instance_Setup.rst:22 bb06f53ddbea4dd09b99e288569f858d +#: ../../Instance_Setup/Instance_Setup.rst:22 df9cbf9e1d024f9386a4b6278c82249e msgid "**Command‑line mode** – launch with all parameters in one command." msgstr "**命令行模式** – 在一个命令中使用所有参数启动。" -#: ../../Instance_Setup/Instance_Setup.rst:24 11bd4b3ce97743e0857c109ae9bc2286 +#: ../../Instance_Setup/Instance_Setup.rst:24 5f7cba6df26144e9840d140f94065b33 msgid "" "**Persistent configuration** – stores the latest instance definitions in " "``setup.json`` (same directory as the script). Each type (server/client) " @@ -62,14 +62,14 @@ msgstr "" "**持久配置** – " "将最新的实例定义存储在“setup.json”(与脚本相同的目录)中。每种类型(服务器/客户端)仅保留一个配置,该配置在每次更新时都会被覆盖。" -#: ../../Instance_Setup/Instance_Setup.rst:29 7b3f201c93c349719f4128f05cc194bf +#: ../../Instance_Setup/Instance_Setup.rst:29 be9bc008af594306be21e731d6aefee3 msgid "" "**Cross‑platform** – supports Windows (cmd), Linux (gnome‑terminal, xterm, " "or background), and macOS (Terminal.app)." msgstr "" "**跨平台** – 支持 Windows (cmd)、Linux(gnome 终端、xterm 或后台)和 macOS (Terminal.app)。" -#: ../../Instance_Setup/Instance_Setup.rst:32 89b17f1fa60241a288381f74072aa273 +#: ../../Instance_Setup/Instance_Setup.rst:32 57005cb0d04f47b2a8f10ad0bc9c7b43 msgid "" "**Complete parameter support** – all parameters accepted by " "``TCP_Server_Base`` and ``TCP_Client_Base`` can be stored in ``setup.json`` " @@ -78,107 +78,153 @@ msgstr "" "**完整的参数支持** – ``TCP_Server_Base`` 和 ``TCP_Client_Base`` " "接受的所有参数都可以存储在``setup.json`` 中以进行微调。" -#: ../../Instance_Setup/Instance_Setup.rst:37 ca859ffa1d4140cab68092170525c59f +#: ../../Instance_Setup/Instance_Setup.rst:37 2ff27919a6f64e8fb610158de5ffdf69 msgid "Usage" msgstr "用法" -#: ../../Instance_Setup/Instance_Setup.rst:40 1935db82ef7f4dbeb08d386a49aba876 +#: ../../Instance_Setup/Instance_Setup.rst:40 dddde4f2d7084bc48efa1296cfd3f22c msgid "Interactive Mode" msgstr "互动模式" -#: ../../Instance_Setup/Instance_Setup.rst:42 86f8dafb1c9d4adaa4666b1dea52af23 +#: ../../Instance_Setup/Instance_Setup.rst:42 f060e64959674f52b223bbc4b01d568c msgid "Run the script without any arguments:" msgstr "不带任何参数运行脚本:" -#: ../../Instance_Setup/Instance_Setup.rst:48 d516fe21071e448da15df91f07353fc8 +#: ../../Instance_Setup/Instance_Setup.rst:48 dd76918a5ffd44838c1b0c44595ae55b msgid "The script will ask you to:" msgstr "该脚本将要求您:" -#: ../../Instance_Setup/Instance_Setup.rst:50 41684f03f6d7460f87fed8f4bef8f1e5 +#: ../../Instance_Setup/Instance_Setup.rst:50 80847dcc3e404a779b2e693cf8de50d5 msgid "Choose the type (0 for Server, 1 for Client)." msgstr "选择类型(0 表示服务器,1 表示客户端)。" -#: ../../Instance_Setup/Instance_Setup.rst:51 750440862d7545e9bc4f137f36983fd6 +#: ../../Instance_Setup/Instance_Setup.rst:51 cebc8ba62d1a48a69d8019c3862dc18b msgid "Enter the bind address and port (``host:port``)." msgstr "输入绑定地址和端口(``host:port``)。" -#: ../../Instance_Setup/Instance_Setup.rst:52 a938572ed1dd43f7988e382ec9fea306 +#: ../../Instance_Setup/Instance_Setup.rst:52 4311b66e1b974b4596f2693908ba3109 msgid "If Client, also enter the server address and port to connect to." msgstr "如果是客户端,还需输入要连接的服务器地址和端口。" -#: ../../Instance_Setup/Instance_Setup.rst:53 3f30b7f3c46e42c19eae6344b424e1f7 +#: ../../Instance_Setup/Instance_Setup.rst:53 65c194c1b655417c921c1e03daf7a17c msgid "" "Decide whether to add another instance (if you add the same type again, the " "previous configuration of that type is overwritten)." msgstr "决定是否添加其他实例(如果再次添加相同类型,则覆盖该类型之前的配置)。" -#: ../../Instance_Setup/Instance_Setup.rst:55 e8b6550fe5eb426f8478c1b3fef81160 +#: ../../Instance_Setup/Instance_Setup.rst:55 6bb60d44328244d4ab20f3ae5ba2b827 msgid "" "If ``setup.json`` already exists, you will be prompted to either reuse the " "existing configuration (launch the stored instances) or overwrite it with " "new definitions." msgstr "如果“setup.json”已存在,系统将提示您重用现有配置(启动存储的实例)或使用新定义覆盖它。" -#: ../../Instance_Setup/Instance_Setup.rst:60 b222bf87298747c6b526d72e1459ad83 +#: ../../Instance_Setup/Instance_Setup.rst:60 9aeb2efa913341a89212e86cb7419e5a msgid "" "**Important:** When you choose to overwrite, the old server/client " "configuration is **completely replaced** by the new one. There is no " "merging." msgstr "**重要提示:** 当您选择覆盖时,旧的服务器/客户端配置将被新的配置**完全替换**。没有合并。" -#: ../../Instance_Setup/Instance_Setup.rst:65 ad560cd1d404495583c74b5929235453 +#: ../../Instance_Setup/Instance_Setup.rst:65 b5cd8be69a40420a89cf7ad69631cd45 msgid "Command‑line Mode" msgstr "命令行模式" -#: ../../Instance_Setup/Instance_Setup.rst:67 50a9125f52a54c9b82c66cf616e06d71 +#: ../../Instance_Setup/Instance_Setup.rst:67 d798ec606de2454da5f10e9d0a7e677c msgid "Use the following options:" msgstr "使用以下选项:" -#: ../../Instance_Setup/Instance_Setup.rst:82 f7f3b3263fdf457c908ea18b74dccea9 +#: ../../Instance_Setup/Instance_Setup.rst:70 4d5c1aebbede473c8598cbc45f8deb20 +msgid "Option" +msgstr "选项" + +#: ../../Instance_Setup/Instance_Setup.rst:70 a50ba126b168482997fae00497252fd4 +msgid "Description" +msgstr "描述" + +#: ../../Instance_Setup/Instance_Setup.rst:72 7755904907ee46f6a34144360ed876f7 +#, python-brace-format +msgid "``--type {0,1}``" +msgstr "`` --type {0,1} ``" + +#: ../../Instance_Setup/Instance_Setup.rst:72 e0533cce8d9f4586aa838073b1ed3e2a +msgid "**Required.** 0 = Server, 1 = Client." +msgstr "* *必需。* * 0 =服务器, 1 =客户端。" + +#: ../../Instance_Setup/Instance_Setup.rst:74 7cc185b424ee489282d9d85571d54fae +msgid "``--setup_addr_port``" +msgstr "`` --setup_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:74 37df8730dce9408a87ba48b775eb2b0a +#, fuzzy +msgid "**Required.** Bind address and port (e.g. ``127.0.0.1:8000``)." +msgstr "输入绑定地址和端口(``host:port``)。" + +#: ../../Instance_Setup/Instance_Setup.rst:77 388a3dd952ac47a78598792ef7d587bc +msgid "``--connect_addr_port``" +msgstr "`` --connect_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:77 b4f319ec304e4a26a09ac62b90db0a07 +#, fuzzy +msgid "Required for Client only. Server address and port to connect to." +msgstr "如果是客户端,还需输入要连接的服务器地址和端口。" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7bb925846a36488d945c3442889d83c0 +msgid "``--setup_num``" +msgstr "`` --setup_num ``" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7f3e0b1925924583acf7acb25ec6e183 +msgid "" +"*Ignored.* The script always launches a single instance. This flag is " +"accepted for compatibility but has no effect." +msgstr "*忽略。*脚本始终启动单个实例。此标志因兼容性而被接受,但无效。" + +#: ../../Instance_Setup/Instance_Setup.rst:86 99952fa68b694654b82a309a26419152 msgid "Examples" msgstr "示例" -#: ../../Instance_Setup/Instance_Setup.rst:84 13847166f8d54c9db943e1e55cf66c55 +#: ../../Instance_Setup/Instance_Setup.rst:88 9c77e76f40a940e99e754d27e3bc1b05 msgid "**Launch a single server** on ``127.0.0.1:8000``:" msgstr "**在“127.0.0.1:8000”上启动单个服务器**:" -#: ../../Instance_Setup/Instance_Setup.rst:90 aa92290b83324661a23d2343f7ca56fb +#: ../../Instance_Setup/Instance_Setup.rst:94 37df8730dce9408a87ba48b775eb2b0a msgid "" "**Launch a client** bound to port ``9000``, connecting to a server at " "``127.0.0.1:8000``:" msgstr "**启动绑定到端口“9000”的客户端**,连接到“127.0.0.1:8000”的服务器:" -#: ../../Instance_Setup/Instance_Setup.rst:97 9d2ca3944f984434b4ecb579b075ccc4 +#: ../../Instance_Setup/Instance_Setup.rst:101 +#: 0bdae3cd584e464ab526840aa032e3a4 msgid "" "**Launch from an existing configuration** (if ``setup.json`` is present):" msgstr "**从现有配置启动**(如果存在“setup.json”):" -#: ../../Instance_Setup/Instance_Setup.rst:105 -#: d88aa64a570f4f36baf5c7c92d4bd861 +#: ../../Instance_Setup/Instance_Setup.rst:109 +#: ff45af42eb7d4d3faa8640a18bfd61f6 msgid "Configuration File" msgstr "配置文件" -#: ../../Instance_Setup/Instance_Setup.rst:107 -#: cfb0db3d5e23418d9c93ddb2d34548d4 +#: ../../Instance_Setup/Instance_Setup.rst:111 +#: 39e34a4760354d78b87a8e050029e197 msgid "" "The script writes a file named ``setup.json`` in the same directory. Its " "structure is:" msgstr "该脚本在同一目录中写入一个名为“setup.json”的文件。其结构为:" -#: ../../Instance_Setup/Instance_Setup.rst:131 -#: 0a2b6c0e312a49de8ed7838e045b3b66 +#: ../../Instance_Setup/Instance_Setup.rst:135 +#: 9e72e7ecd43c498299e24f5598b6f2b8 msgid "" "**Each list contains at most one object.** When a new server or client " "configuration is added, the entire list for that type is replaced." msgstr "**每个列表最多包含一个对象。** 添加新的服务器或客户端配置时,该类型的整个列表都会被替换。" -#: ../../Instance_Setup/Instance_Setup.rst:136 -#: 1a256391599446d99c3cc375639f251c +#: ../../Instance_Setup/Instance_Setup.rst:140 +#: 8731a083a201487bb579beb4a238a0db msgid "Custom Parameters" msgstr "自定义参数" -#: ../../Instance_Setup/Instance_Setup.rst:138 -#: 36cefa4820cd42ee93fdb560be1b1032 +#: ../../Instance_Setup/Instance_Setup.rst:142 +#: 1568543fd757484da86f12a72ccb58b5 msgid "" "You can manually edit ``setup.json`` to include any parameter accepted by " "``TCP_Server_Base`` or ``TCP_Client_Base`` (see the source code for the full" @@ -192,68 +238,84 @@ msgstr "" "您可以手动编辑“setup.json”以包含“TCP_Server_Base”或“TCP_Client_Base”接受的任何参数(完整列表请参阅源代码)。当启动器覆盖配置时,这些自定义值将被保留(因为脚本读取现有配置并使用用户提供的值更新它,但如果您选择覆盖,旧配置将被丢弃,仅保存新字段" " - 因此,如果您需要自定义参数,您应该在首次启动后添加它们或手动编辑文件)。" -#: ../../Instance_Setup/Instance_Setup.rst:150 -#: 6b90a320b39147c7ac5287c51b028da3 +#: ../../Instance_Setup/Instance_Setup.rst:154 +#: c62b1bfe8ceb40f8865a34ac87b4ba18 msgid "Extension Protocols and Startup Mode" msgstr "扩展协议和启动模式" -#: ../../Instance_Setup/Instance_Setup.rst:152 -#: 81cf1090db0240e19aecfb16cdafb622 +#: ../../Instance_Setup/Instance_Setup.rst:156 +#: e6a7dcb4ea184e828e6667ae6505d872 msgid "" "Two extension protocols ship with the launcher and are loaded automatically " "for every instance whose ``setup.json`` entry sets " "``is_extend_command=True``:" msgstr "启动器附带两个扩展协议,并为每个“setup.json”条目设置“is_extend_command=True”的实例自动加载:" -#: ../../Instance_Setup/Instance_Setup.rst:156 -#: e89028b886064e8ab1e94e5e6a6cdaa0 -msgid "``command_control_extension_tcp.py`` – remote command" +#: ../../Instance_Setup/Instance_Setup.rst:160 +#: 7159f768fc9a4ac199bfe0a4d8478fba +#, fuzzy +msgid "" +"``command_control_extension_tcp.py`` – remote command execution with per-" +"client log collection (``/command``)." msgstr "``command_control_extension_tcp.py`` – 远程命令" -#: ../../Instance_Setup/Instance_Setup.rst:157 -#: 86a457e4e5bf4791bcbef5bb12612391 +#: ../../Instance_Setup/Instance_Setup.rst:162 +#: d2d277053c74470abc949b891493a5f0 +#, fuzzy msgid "" -"execution with per-client log collection (``/command``). - " -"``forward_extension_tcp.py`` – forwarding messages, files, multiple files, " -"folders and multiple folders to any number of destination clients " -"(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " -"``/folder_forward``, ``/multiple_folder_forward``)." +"``forward_extension_tcp.py`` – forwarding files, multiple files, folders and" +" multiple folders to any number of destination clients (``/file_forward``, " +"``/multiple_file_forward``, ``/folder_forward``, " +"``/multiple_folder_forward``)." msgstr "" "使用每个客户端日志收集执行(``/command``)。 -``forward_extension_tcp.py`` - " "将消息、文件、多个文件、文件夹和多个文件夹转发到任意数量的目标客户端(``/send_msg_forward``、``/file_forward``、``/multiple_file_forward``、``/folder_forward``、``/multiple_folder_forward``)。" -#: ../../Instance_Setup/Instance_Setup.rst:164 -#: baee7200e5634507a28b2f69309c3c49 +#: ../../Instance_Setup/Instance_Setup.rst:168 +#: a8194174e5fd4dcf87ede716eaada9a4 +msgid "" +"Plain-message forwarding is native to the TCP protocol (no extension " +"needed): the client-only command ``/forward_send_msg`` relays messages to " +"the listed destination clients through the server." +msgstr "" +"纯消息转发是TCP协议的原生(不需要扩展) :仅客户端命令``/forward_send_msg ``通过服务器将消息中继到列出的目标客户端。" + +#: ../../Instance_Setup/Instance_Setup.rst:173 +#: 98297b76f3404736b92b6c507726a50a msgid "" "With ``is_extend_command=False`` (the default) only the raw TCP protocol is " "started." msgstr "使用“is_extend_command=False”(默认)仅启动原始 TCP 协议。" -#: ../../Instance_Setup/Instance_Setup.rst:167 -#: f140a881acba4933bd83af5bb35737d1 +#: ../../Instance_Setup/Instance_Setup.rst:176 +#: 9b5ab0319a9843f1b39721d3a96d1777 msgid "" "The ``is_input_command_in_console`` flag selects how the instance is " "started:" msgstr "``is_input_command_in_console`` 标志选择实例的启动方式:" -#: ../../Instance_Setup/Instance_Setup.rst:170 -#: a40aa238baac4503a0dc0c9cea0745ee -msgid "``True`` (default) – ``start_TCP_Server()`` /" -msgstr "``True``(默认)-``start_TCP_Server()`` /" +#: ../../Instance_Setup/Instance_Setup.rst:179 +#: 1ec730c995b1454da1b208331d9ca8fe +msgid "" +"``True`` (default) – ``start_TCP_Server()`` / ``start_TCP_client()`` is " +"called directly and the console input loop runs in its own thread." +msgstr "" +"`` True `` (默认) – `` START_TCP_SERVER () ``/`` START_TCP_CLIENT () " +"``被直接调用,控制台输入循环在其自己的线程中运行。" -#: ../../Instance_Setup/Instance_Setup.rst:171 -#: fca5c2c0fe914740a931957b30a9927b +#: ../../Instance_Setup/Instance_Setup.rst:182 +#: 3cfb2eba312943cea85388f4e9faf40d +#, fuzzy msgid "" -"``start_TCP_client()`` is called directly and the console input loop runs in" -" its own thread. - ``False`` – the instance runs in a background thread and " -"the launcher keeps the process alive until the instance stops (useful for " -"headless deployments)." +"``False`` – the instance runs in a background thread and the launcher keeps " +"the process alive until the instance stops (useful for headless " +"deployments)." msgstr "" "直接调用“start_TCP_client()”,控制台输入循环在其自己的线程中运行。 -``False`` - " "实例在后台线程中运行,启动器使进程保持活动状态,直到实例停止(对于无头部署很有用)。" -#: ../../Instance_Setup/Instance_Setup.rst:177 -#: c2925f0f85154040bcdd1252a228fd7f +#: ../../Instance_Setup/Instance_Setup.rst:186 +#: 3b3482fa903f4afd81234e588a229c1f msgid "" "Both extensions also expose injectable registration " "(``setup_server_commands(instance)`` / ``setup_client_commands(instance)``) " @@ -267,62 +329,67 @@ msgstr "" "is_input_command_in_console=True)`` /``server_setup(instance=None, " "is_input_command_in_console=True)`` 接受现有实例,因此可以加载多个扩展从代码到同一个实例。" -#: ../../Instance_Setup/Instance_Setup.rst:186 -#: e58290a2c5f3492bafa47f15b7c2958f +#: ../../Instance_Setup/Instance_Setup.rst:195 +#: 1dc7b46c0e3047d2885573cc4f55e7c6 msgid "Internal Operation" msgstr "内部运作" -#: ../../Instance_Setup/Instance_Setup.rst:188 -#: 6655764b01644b92910338c01d38b0cb -msgid "Each instance is launched in a new terminal window" +#: ../../Instance_Setup/Instance_Setup.rst:197 +#: 8870bc6b731d40a095a40faed0c8f16d +#, fuzzy +msgid "" +"Each instance is launched in a new terminal window (or background process)." msgstr "每个实例都在新的终端窗口中启动" -#: ../../Instance_Setup/Instance_Setup.rst:189 -#: 6824028a99254c77a55fb56d51b5e9a5 -msgid "(or background process)." -msgstr "(或后台进程)。" - -#: ../../Instance_Setup/Instance_Setup.rst:190 -#: 0a09353648d44ba2a840fc1cf8a850bc -msgid "The configuration is passed via a temporary JSON" +#: ../../Instance_Setup/Instance_Setup.rst:199 +#: 13c720a93ba54e8ca330fce3a078bb5b +#, fuzzy +msgid "" +"The configuration is passed via a temporary JSON file to avoid shell " +"escaping issues." msgstr "配置通过临时 JSON 传递" -#: ../../Instance_Setup/Instance_Setup.rst:191 -#: f6327281e35a4304b404563d00301a1a -msgid "file to avoid shell escaping issues." -msgstr "文件以避免 shell 转义问题。" - -#: ../../Instance_Setup/Instance_Setup.rst:192 -#: 997e364f35714b18b1e47af641ce58db -msgid "If an instance fails to start, the error is" -msgstr "如果实例无法启动,错误为" - -#: ../../Instance_Setup/Instance_Setup.rst:193 -#: 4404c088296e4c35b49ed59eca3b6678 -msgid "displayed and the window pauses for inspection." +#: ../../Instance_Setup/Instance_Setup.rst:201 +#: 9566eb3cb70e4b77bcbdb584f7e0b6fc +#, fuzzy +msgid "" +"If an instance fails to start, the error is displayed and the window pauses " +"for inspection." msgstr "显示并且窗口暂停以进行检查。" -#: ../../Instance_Setup/Instance_Setup.rst:196 -#: 2b8f982006184d36b06d4ac5579a36f4 +#: ../../Instance_Setup/Instance_Setup.rst:205 +#: 5f0f0f6a41384356986a1182f14a0a4c msgid "Requirements" msgstr "要求" -#: ../../Instance_Setup/Instance_Setup.rst:198 -#: f10fabd9883942b780cd0822da245837 +#: ../../Instance_Setup/Instance_Setup.rst:207 +#: 49ceb74e41104b00971cd9edecef08ae msgid "Python 3.6+" msgstr "Python 3.6+" -#: ../../Instance_Setup/Instance_Setup.rst:199 -#: f464e1a571fa46388937f3236b36d3bf +#: ../../Instance_Setup/Instance_Setup.rst:208 +#: 541cd1be44c044f3880eb220a87c06b7 msgid "The ``network_api.connect_tcp`` module must be" msgstr "``network_api.connect_tcp`` 模块必须是" -#: ../../Instance_Setup/Instance_Setup.rst:200 -#: fc6ca227cd904333916e81b4a7093acc +#: ../../Instance_Setup/Instance_Setup.rst:209 +#: 427fef8d7c5047fc934681026969639a msgid "importable (the script imports ``TCP_Server_Base``" msgstr "可导入(脚本导入``TCP_Server_Base``" -#: ../../Instance_Setup/Instance_Setup.rst:201 -#: dcc9c410853247b0a3302c2c41f6fe13 +#: ../../Instance_Setup/Instance_Setup.rst:210 +#: fb35af261d664664bdb3c1b3aaac3e83 msgid "and ``TCP_Client_Base`` from there)." msgstr "和来自那里的“TCP_Client_Base”)。" + +#~ msgid "``True`` (default) – ``start_TCP_Server()`` /" +#~ msgstr "``True``(默认)-``start_TCP_Server()`` /" + +#~ msgid "(or background process)." +#~ msgstr "(或后台进程)。" + +#~ msgid "file to avoid shell escaping issues." +#~ msgstr "文件以避免 shell 转义问题。" + +#~ msgid "If an instance fails to start, the error is" +#~ msgstr "如果实例无法启动,错误为" diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.add_extension.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.add_extension.po new file mode 100644 index 0000000..24d189a --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.add_extension.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.add_extension.rst:2 239fa5edd0d944bd884b21d803e0f4a1 +msgid "PyFlow.add\\_extension module" +msgstr "" + +#: PyFlow.add_extension.copy_extension_files:1 c4a96a18a6754f1fb4c5c3a9ff9dd518 +#: of +msgid "Validate extension path(s) and return them as a list." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst 4b8e1285572947c4a34dbcd9e22fb52c +#: 78eb00a995d84b028e95127f753b4fb5 PyFlow.add_extension.remove_extension +#: dff4aabd6aad43e188c9fc9b73d9142b of +msgid "Parameters" +msgstr "" + +#: 49c2e6f1496d433ab7a9d162802419f1 5de3fc7a955443cb9bef23851780925f +#: PyFlow.add_extension.add_extension:3 +#: PyFlow.add_extension.copy_extension_files:3 +#: PyFlow.add_extension.remove_extension:3 b82537ecca1e4f418a5a6aacacdd900c of +msgid "a single path string or a list of path strings." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst fa91fb96a98f4374b2da5085d1373ef4 +msgid "Returns" +msgstr "" + +#: 450992bbbe91432fb44b0265e58d8f59 PyFlow.add_extension.copy_extension_files:5 +#: of +msgid "The original paths as a list (extensions are not copied)." +msgstr "" + +#: 487ed0fee55540c796c92b88d0a8b2ea +#: PyFlow.add_extension.add_added_extension_logs:1 of +msgid "Append paths to the extension registration log file." +msgstr "" + +#: PyFlow.add_extension.add_extension:1 cd718206d465487ebd43c17e796c4da5 of +msgid "Register extension file(s) in added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.remove_extension:1 bb4313796aff49a1b96a37d66912f18a of +msgid "Remove registered extension path(s) from added_extensions.json." +msgstr "" + +#: 6520ce551e5f40e6afe59c549d15e493 PyFlow.add_extension.remove_extension:5 of +msgid "If the registration file does not exist, this is a no-op." +msgstr "" + +#: 26339cf4e8a041f798d969a890592971 +#: PyFlow.add_extension.load_registered_extensions:1 of +msgid "Load every registered extension from added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.load_registered_extensions:3 +#: e53cceb4fe384c1fa10ddf1905998823 of +msgid "" +"For each registered path, the module is imported dynamically and its " +"``setup_server_commands(instance)`` or " +"``setup_client_commands(instance)`` is called, depending on " +"*instance_type*." +msgstr "" + +#: 11efbd89b0254bb190c21005396f53dd +#: PyFlow.add_extension.load_registered_extensions:7 of +msgid "" +"Raises ImportError if the JSON file is reachable but a module cannot be " +"imported or loaded, or if the required setup function is missing." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po new file mode 100644 index 0000000..7953f5f --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po @@ -0,0 +1,36 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.command_control_extension_tcp.rst:2 +#: a6084f2941f34f579f28c55e9dfb768d +msgid "PyFlow.command\\_control\\_extension\\_tcp module" +msgstr "" + +#: 9209095513ec402980427a0ddd988219 +#: PyFlow.command_control_extension_tcp.setup_server_commands:1 of +msgid "Register the control-extension commands on a server instance." +msgstr "" + +#: 114b0a54c29747fd88bc2852eab174bf +#: PyFlow.command_control_extension_tcp.setup_client_commands:1 of +msgid "Register the control-extension commands on a client instance." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.flow_setup.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.flow_setup.po new file mode 100644 index 0000000..72660d1 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.flow_setup.po @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.flow_setup.rst:2 a6a54598744740409fe0b28f73834ffe +msgid "PyFlow.flow\\_setup module" +msgstr "" + +#: 586460f36f3b4654abb0db6b5d77b39d PyFlow.flow_setup.launch_web_tool:1 of +msgid "Launch the transfer_web launcher (``kind`` = \"server\" or \"client\")." +msgstr "" + +#: PyFlow.flow_setup.launch_web_tool:3 ef1274ba70124aeeadee3d4cfcca99c2 of +msgid "" +"The web tool is a Flask app that opens a browser UI, so it runs in its " +"own process (a terminal window when one is available, otherwise detached)" +" and the launcher returns immediately." +msgstr "" + +#: 7086887643164f229f919546dceb0e36 PyFlow.flow_setup.edit_existing_instances:1 +#: of +msgid "Vim-style editor to delete/change existing instances." +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:3 d04ffe1cf853403292908fc69962f7dc +#: of +msgid "Returns (status, servers, clients):" +msgstr "" + +#: 5a751731515c4adfba9787fdd5e93215 PyFlow.flow_setup.edit_existing_instances:4 +#: of +msgid "status == \"saved\" -> setup.json was written (:w / :wq); keep the" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:5 e61a6437dae14d1e9f9ac4e6848cd444 +#: of +msgid "returned edited lists" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:6 bb9424a8ea894dbb888df4fa2e0875ed +#: of +msgid "status == \"discarded\" -> the editor was exited without saving" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:7 d0d81a426f754475922c3e9f4aee6627 +#: of +msgid "(:q! / :q) and the original lists are returned unchanged" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po new file mode 100644 index 0000000..3ec8d0d --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.forward_extension_tcp.rst:2 +#: 39b0ca283c7c48b08f66a67eb80766b0 +msgid "PyFlow.forward\\_extension\\_tcp module" +msgstr "" + +#: PyFlow.forward_extension_tcp:1 ad6b75f791c543dea48894cd8bac6941 of +msgid "Forward extension for the TCP protocol." +msgstr "" + +#: PyFlow.forward_extension_tcp:3 ff4264b5fba24d7d93e9bb3e398edfd5 of +msgid "" +"Disk-based, upload-then-push forwarding of files and folders to a list of" +" destination clients. This is deliberately a second implementation of " +"file forwarding: the native TCP protocol already streams files and " +"folders in memory (``/forward_file`` / ``/forward_folder`` on a client " +"console, relayed by the server as ``/forward_item`` with no disk I/O on " +"the server), while this extension uploads the data to the server's " +"transfer directory first and then asks the server to push the stored " +"copies. Plain-message forwarding is native as well (the client-only " +"command ``/forward_send_msg``, relayed by the server), so no string " +"forwarding lives here." +msgstr "" + +#: 2f49002886224f5bb68eb09f2c4a8a30 PyFlow.forward_extension_tcp:14 of +msgid "Transfer families added by this extension:" +msgstr "" + +#: PyFlow.forward_extension_tcp:16 ab1c45d0c28345c2898a54c66c98416b of +msgid "/file_forward <(ip, port)> ..." +msgstr "" + +#: 93ef2915b1fa4bd9b4b42e8e6ec747f8 PyFlow.forward_extension_tcp:17 of +msgid "forward one file to every listed destination" +msgstr "" + +#: 9773fb5cd9414f0aabb496ae3a32005a PyFlow.forward_extension_tcp:18 of +msgid "/multiple_file_forward ... <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:19 ee54881259274d6b9085bf71b44d659a of +msgid "forward several files to every listed destination" +msgstr "" + +#: 19144093ca8d45e988dfcfe09221bf6a PyFlow.forward_extension_tcp:20 of +msgid "/folder_forward <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:21 b73d9263c9874b1e9ea5a714138d72d8 of +msgid "forward one folder (structure preserved) to every destination" +msgstr "" + +#: 4f7e8bb4b31c4eddbb78d815e85f8795 PyFlow.forward_extension_tcp:22 of +msgid "/multiple_folder_forward ... <(ip, port)> ..." +msgstr "" + +#: 9e95e860d39744f8ba3d52ddaf357943 PyFlow.forward_extension_tcp:23 of +msgid "forward several folders to every listed destination" +msgstr "" + +#: 612162f14a574132bf396a1b6ea853f1 PyFlow.forward_extension_tcp:25 of +msgid "" +"Items come first, destinations last; every destination is written as a " +"Python address tuple, e.g. ``\"('127.0.0.1', 3000)\"``. There is no limit" +" on the number or size of items or destinations." +msgstr "" + +#: 1b0ec7328493432fb876117b1f76bb3b PyFlow.forward_extension_tcp:29 of +msgid "" +"The commands are only available on the client console: they are " +"registered in the \"client\" handler group, so typing them on the server " +"console is rejected as an unrecognized command. Forwarding goes through " +"the server - the client uploads the data over the normal transfer channel" +" (the server stores it in its transfer directory) and then asks the " +"server to push it to the destinations, which receive it through the main " +"protocol's own receive paths. Destinations that are unreachable (not " +"connected to the server, or the server itself, which is never in the " +"client table) are skipped and the remaining destinations are still " +"served." +msgstr "" + +#: 443d9fe7a21148d984a0e3f2f3cb2c28 +#: PyFlow.forward_extension_tcp.setup_client_commands:1 of +msgid "Register the file/folder forward commands on a client instance." +msgstr "" + +#: 5dbc099afeb54fbb859bf5760331adfd +#: PyFlow.forward_extension_tcp.setup_client_commands:3 of +msgid "" +"Message forwarding (``/forward_send_msg``) is native and needs no setup. " +"Each command binds its transfer kind and single/multiple policy into the " +"shared handler via functools.partial; where_to_run=\"client\" makes them " +"fire from console input only." +msgstr "" + +#: 392556d0850f44b1b64dae0fe65a748c +#: PyFlow.forward_extension_tcp.setup_server_commands:1 of +msgid "Register the file/folder forward relays on a server instance." +msgstr "" + +#: 399725c6728441568702225abc50b2c4 +#: PyFlow.forward_extension_tcp.setup_server_commands:3 of +msgid "" +"The message relay (``/forward_send_msg``) is native and needs no setup. " +"These handlers are triggered by relay requests sent by clients, i.e. they" +" live in the \"server\" group: messages coming in from other instances " +"are dispatched there. The /xxx_forward commands themselves stay in the " +"client group, so typing them on the server console is rejected as " +"unrecognized." +msgstr "" + +#: 337c29db84fe4d3099a2e3d5e135d0d1 PyFlow.forward_extension_tcp.client_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable client (mirrors the control " +"extension)." +msgstr "" + +#: 99ee11955438459d84f5c1ae6f3fedbb PyFlow.forward_extension_tcp.server_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable server (mirrors the control " +"extension)." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po new file mode 100644 index 0000000..5a34ff3 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po @@ -0,0 +1,1759 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_tcp.rst:2 +#: 1e4cd09ce3ef45ceb95b8eb81eb7493b +msgid "PyFlow.network\\_api.connect\\_tcp module" +msgstr "" + +#: 25b88cdb337c44539a843c674ba6825b PyFlow.network_api.connect_tcp:1 of +msgid "" +"TCP transport for PyFlow: the server and client base classes and the wire" +" parsers." +msgstr "" + +#: 1f1482fc5c2849a68f34c74a8b55ab79 PyFlow.network_api.connect_tcp:3 of +msgid "" +"``TCP_Server_Base`` accepts connections and dispatches inbound lines; " +"``TCP_Client_Base`` connects, sends and reads on the same conventions:" +msgstr "" + +#: 76e93df1b95547ff8bded710dc31340d PyFlow.network_api.connect_tcp:6 of +msgid "" +"one message per line, terminated by a newline; a line that starts with " +"``/`` is a command and goes to the command handlers, anything else is a " +"plain message reported to the registered message listeners;" +msgstr "" + +#: 2497ecf21ac947c78502a3839a452cb8 PyFlow.network_api.connect_tcp:9 of +msgid "" +"an RSA-encrypted channel is negotiated right after connect unless " +"``is_enable_encrypto`` is False;" +msgstr "" + +#: 9702905f1f8a4750905b2ffb96f99ec8 PyFlow.network_api.connect_tcp:11 of +msgid "" +"file/folder transfer, message forwarding and port allocation are layered " +"on the same socket and share its command namespace." +msgstr "" + +#: 4cc5743bdfe54148ad38f54f10e688c9 PyFlow.network_api.connect_tcp:14 of +msgid "" +"The forwarding extensions use the module-level parsers " +"`parse_forwarded_message`, `parse_forward_items_and_addrs`, " +"`parse_forward_originator` and `forward_skip_message`." +msgstr "" + +#: 49796529cb7f49ee8131b257212b5420 PyFlow.network_api.connect_tcp:18 of +msgid "" +"Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and " +"``TCP_Client_APIs.rst``; argument, return and exception contracts live in" +" the docstrings below." +msgstr "" + +#: 39274682c4bd46489ed6fe535b50ede5 +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:1 of +msgid "Split a ``/send_msg_from `` relay envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07da14a120314eb7a3acd1c46ddfeaca 0b005c95146946208a01640628f50a57 +#: 10904afb15ee45b29e91a2ab5da0db26 14d9353e52a54c019428fb94211f17f6 +#: 1861e8bcd5bb41489f13265860311349 1c6ba1e0968a409499b7a262c34dcc2a +#: 20b95160870648538fd617ca4ce3d2b5 2351fc654dfc452a8d07e5d566265d9f +#: 27611fb8240a422db411c043f60bdbe9 464f4d62231e4a61b1a939ad2054b13c +#: 48bd66821c14451d9a0c682d9468a5a5 4e1c4be3e22f403396d5ee1788d0e3b0 +#: 4fa9ba6e763440b8ab00832520b4d305 501a0caef71d436e84469bc1ef9c1e5f +#: 52bdf8d502cc4a31a6df5ae543ceb182 531f1e6ff68e44bcac61633e2d7c511f +#: 5404b8d0f66848c3b1db9f08a25f92c0 55fc240d149746e190ecf924a2ca6dd4 +#: 59ab6a93eceb40f7b590133c3d2b8548 6d8d8704b9c94105928dbbdf15ce9f12 +#: 70d6dae11a144ed7b7e9d8e16dbf478c 71de1bb0c4d24062a819a6fe59013d34 +#: 74c64b0da3294322b512f3155955fb7a 76f08e34cd4a42c880880cfb511fcc66 +#: 7a6a9f1497ac48f5867783d78fdae37c 7c3501da0be44d78a671e96ba4384489 +#: 7e9c7fe1ae974c1082550fb3e6e3de1f 7fc19de66098437faef7bffed3b5f752 +#: 7fdf476d99ff4f54ad35cf4bb506e47d 812d9d039fe34f71aa4b662c4511c8e9 +#: 81f8f7d346ff45639f39eb0f033103df 95d8b328540c4094a4fcc8fdc9139645 +#: a163536aaa1f49399502da52fb481666 acdf8b3b266f4eccb6ceca17a110603b +#: b097aecf6ae74e17b36fc9e806d5b26a b3db16a6c2374980ae9b071d9f3f15e6 +#: c2a18448d83f4879bcb29f51ca31bc5b c5ed2382d01042c68a5e372a4c7de2ac +#: d36c8c10afba4732969367886b8663ab d6d746e16dbd47308eea5aacd9614f15 +#: d8e94668c5d7434fa61c2ffcbe73b6af dad1324e962d49419245fe3b88c20121 +#: dd3d356c14cb462e98ea42e3502dca80 eb24a57758194ab2bdef9e95362584df +#: ef5da8d6c91c469fad0871d213999d2a ff3615b7096e46bba8797b739bb954b2 +msgid "Parameters" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:3 +#: e42a6166c82d4861b25cb55f587db28c of +msgid "Received line, e.g. ``/send_msg_from ('127.0.0.1', 3000) hello``." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07f0651994f343d7ac8ee26ae4a45c8e 0b14e887dc524d55affce5a3bd9b9f8d +#: 0cb557ccd8a6423091d0d562f1af31fd 0d1ef94754104f01a11302d72600e73a +#: 1475f9cb68fe417b9de7226406e810b5 14dcda8b059b480982e3fb03399e665b +#: 1d7731490c2f49959618924ddf4327fd 1e40a9184f8544c6b7a1472d55c66a11 +#: 24af9c4562134d16b50c664902b39dff 295718cd92e04380af38e7afaff04010 +#: 34fc7f7760d94c2f8bac262ff821b4d7 451b693e9c414cff9798a5a1592fe9cd +#: 47bbee4bd36c4f998fd04c8cc8d9c199 537ed27e714148da8ea804e3562ccd96 +#: 565b9def256a453784c749fe2ee93bc8 57dee3931014419c91d049fca185ef1b +#: 5dcfbd3c4b13401a82e19373602a4b92 635677a7aff64b79a4f69ffb4c22841c +#: 74b8a4ad713a43f2a14faf494e1886a4 7eca27378af64ce99374c606219ff337 +#: 7f6560a3959c4f069adff9d00ca33f54 855ad029af764579bf4ab14cbd430caa +#: 967377f151e84130a9778ece6228bec2 99035a24f6e94207b84545f8c71a451e +#: a90e5d6a51cd4591afeb3cd934071de0 c72891376fe2481d95c7d9ae3014ae47 +#: ca270cee6d3f4eb7ba331ab841c77f38 cb62202c244d4c08974c674c13a02dca +#: db47465f222c4f3db50a9133de7035f5 eb66bb58c7534346b105203e37b25760 +msgid "Returns" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:7 +#: f25d1962656c4f339805427e2573f84f of +msgid "" +"``(sender_id, payload)`` where ``sender_id`` is the sender's " +"``\"ip:port\"``, or None when the line is not a well-formed envelope." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:9 +#: d0d7af37e4134902ae64e10b2e43b4c1 of +msgid "``(sender_id, payload)`` where ``sender_id`` is the" +msgstr "" + +#: 09b385f0a98640c981d8563427e44d7e +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:10 of +msgid "" +"sender's ``\"ip:port\"``, or None when the line is not a well-formed " +"envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 0acb7e2291644625b8873e997a801b95 140c4a59eb0f43d5b4dc46fc2d3348a7 +#: 1412375394164f17aa5b40b7d9ecf0a4 19eb6fca79324b0791f8ec7488a5246c +#: 21c016ab955e4e6499a97f0ca86684cb 27bd312392a2404099fa44fd8787ed28 +#: 27dcdca3b6b247069ddce775ec471c70 28f7c9db48d949ef930abcedef021138 +#: 28fa8dcc5913460e8906e926c95408b9 2e363ed3ce6746c08ebfbf8bb889b623 +#: 3d654cd4b6b044baa3774f758d61da71 48e6af3ee30f47409e2837867a01ff48 +#: 50da78ed5f904c74a876d7b494241efa 50df226290614747b5568f633a883ae0 +#: 564af23329ae4b29aaea3a3638292f42 571f3b412784479a9bfdcb5034c6a39f +#: 6759afd1efe445adb4970c6265c95995 73e666bc0d164cb6a1506517d9acfdb4 +#: 752e44acb3104f9bbf4d3ff7c8bc244b 76862b0024bd42e2b012c999d92e6969 +#: 7ba45226839e40c8a00e684e7c4e07b9 7cd20d28b6794f34a0f53570fea9546a +#: afead614116b49619f1d29738a18e166 b443d67ba649468ca55f1889f18dd006 +#: bbc7b22bbf5e42f7bae149f481de9f3a c58a75656c6143f8a0cb397f24b619b2 +#: d4f12b17df174b038d554053e034cd2a e669e2ac035448fab6479e209ab51c4d +#: f278b3632fbc43f2b4cbeb7759606a68 f6ee270c707f4805b9035fa772078d19 +msgid "Return type" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:1 +#: f22254d8ca2d47ad8bc9c98664369d61 of +msgid "Split forward-command tokens into items and destination addresses." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:3 +#: b207f60051004834bab91ba392a88437 of +msgid "" +"A token of the form ``('ip', port)`` is a destination, everything else is" +" a forwarded item (message text or a path). Used by the native message " +"forwarding (``/forward_send_msg``) and by the file/folder forward " +"extension." +msgstr "" + +#: 5329fb72f5954768a6cec05ff4cccfea +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:7 of +msgid "Tokens after the command name." +msgstr "" + +#: 2aebf669048046ddb279a935bcfcfde4 +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:10 of +msgid "" +"``(items, addrs)`` in the order given; ``items`` holds texts and " +"paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: 96b96bd19fe94312bddf340fad073cbc +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:12 of +msgid "``(items, addrs)`` in the order given; ``items`` holds texts and" +msgstr "" + +#: 8e82954621d748e2b412952a7fb2753b +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:13 of +msgid "paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:1 +#: a51324658c7943158d90ed706cecc41d of +msgid "Build the console notice for a forward destination that cannot be served." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:3 +#: b541e8446e374bc8b38cd7912a1fa33c of +msgid "Destination ``(ip, port)`` that is unreachable or is the server itself." +msgstr "" + +#: 10e03f21d05e478d9cde0c7176f6e1b8 +#: PyFlow.network_api.connect_tcp.forward_skip_message:7 of +msgid "One-line notice for the console." +msgstr "" + +#: 8005e532fe9d491ca8ddd996889d605d +#: PyFlow.network_api.connect_tcp.parse_forward_originator:1 of +msgid "Extract the originator's ``\"ip:port\"`` from a received transfer command." +msgstr "" + +#: 2cf3052a89c645e782430f1f05fb37a1 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:3 of +msgid "" +"The server's forward relay tags every pushed ``/file`` and " +"``/file_folder`` command with the forwarding client's address tuple; a " +"direct send carries the receiver's own address instead." +msgstr "" + +#: 335f6c1b16104395ba38a33188943d8a +#: PyFlow.network_api.connect_tcp.parse_forward_originator:7 of +msgid "Received transfer command." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:9 +#: e5bd1024ad3f47f19cafc4c83a844b60 of +msgid "" +"This instance's own ``\"ip:port\"``; a command carrying it is a direct " +"send and yields None." +msgstr "" + +#: 65469492f844448fab269b701cbdb704 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:13 of +msgid "" +"Originator ``\"ip:port\"``, or None when the command carries no " +"originator (direct send or non-transfer command)." +msgstr "" + +#: 04a0882a3a5e4099a114728b4d1c79c0 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:15 of +msgid "Originator ``\"ip:port\"``, or None when the command carries no" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:16 +#: a2d34276c4f54c69981f9b51f36cc31d of +msgid "originator (direct send or non-transfer command)." +msgstr "" + +#: 7297b41c47664243b9677b32e21de07e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:1 of +msgid "TCP server: accept clients, dispatch commands, relay messages and files." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:3 +#: f5119fd32b60425bb33e4aa148ff7139 of +msgid "" +"Each accepted connection is served by `handle_client` in its own thread: " +"a line starting with ``/`` goes to `handle_command` (built-in commands " +"plus the handlers registered with `register_command`), any other line is " +"a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``." +msgstr "" + +#: 2870b0163d68487daf7526e91eb49ff6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:11 of +msgid "Address the server socket binds to." +msgstr "" + +#: 007c3d5fac544fd480ac10cc56c2f236 0929a7309fef40cab69e9dc22c376ef2 +#: 2e995e9363c54a7ba267ff3a53e10908 507062a8930e432dbcab31b039724b27 +#: 66f4496879ee419d8d748faf8cff0f05 75fcffd8750942559c93c5e590a10503 +#: 83c05847c82b492090a9279f5072434d 8e64315489764ceba6e1120da4675ffb +#: 9c28c79f3bb84af98029a17c0ffc84cd +#: PyFlow.network_api.connect_tcp.TCP_Client_Base +#: PyFlow.network_api.connect_tcp.TCP_Server_Base +#: cf754e752b6c445989b3301b807fc9eb d61fb222783b4f898eb1e757631a1c9a of +msgid "type" +msgstr "" + +#: 2dcb98253fbf428b96fdb5b720769c02 568be916ae0340289a6569e718e7cbf2 +#: 904732a26bcc4835bd3429f0015ae8a4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:14 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:26 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:13 of +msgid "str" +msgstr "" + +#: 6ac74cca7b0f443ca89f3d2c3bbe1aaa +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:17 of +msgid "First port considered for binding and for allocation." +msgstr "" + +#: 0f9991451920470fa3c8a74228b46bb0 9ecf88c5193c470b98dc95d184c94f92 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:19 of +msgid "int" +msgstr "" + +#: 6746407aac194c4880c7cbf82b7fa2fb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:23 of +msgid "" +"Accepted connections keyed by ``(ip, port)``; each value holds " +"``socket``, ``address``, ``id`` and ``connected_time``." +msgstr "" + +#: 7ba2aaf3fe4c42739121eb3ca2bcc79a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:26 of +msgid "dict" +msgstr "" + +#: 93556efee2aa445684af3fdc1532548a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:30 of +msgid "True while the accept loop runs." +msgstr "" + +#: 0ab0667a38254203929e173ab5024f1d 76745b91cce345b092b4e5d931d71126 +#: 839584ae46a0488a95917b35e253768a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:38 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:44 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:32 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:38 +#: f692af2051ae43cdae389a97cd91e9d7 of +msgid "bool" +msgstr "" + +#: 4ad873e4e9da4574bc068029455f62ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:36 +#: fae1c5fe3a844b69a2c7ec611073ecf9 of +msgid "Whether the RSA channel is negotiated." +msgstr "" + +#: 37e6dd691f544a80b9e0949883a2e5a8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:1 of +msgid "Create the server and, unless extended, start accepting clients." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:3 +#: bf58d54a167d45de9a18641877ada912 of +msgid "Address the server socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: 2f4d0c4bfd8246d4984d4c843510775d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:6 of +msgid "First port to bind; also the base of the allocation range." +msgstr "" + +#: 48b1e43c9f2a4e81a9c760ca0d80bad0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:8 of +msgid "Maximum concurrent clients. Defaults to 10." +msgstr "" + +#: 8786a9d2b6b449209be813778d785712 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:10 of +msgid "Step between candidate ports. Defaults to 1." +msgstr "" + +#: 15c0118df8da4454a7fea8d529989237 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:12 of +msgid "Number of ports per step. Defaults to 100." +msgstr "" + +#: 973382c423524d44a569c318399e0b99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:14 +#: d5f8f341d98d478289d9a226f37c1003 of +msgid "Concurrent file transfers allowed. Defaults to 10." +msgstr "" + +#: 4fe9a7607dbc4eebbce4e62f9306c76f +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:17 of +msgid "" +"Reserve a port range across processes, so several instances on one host " +"do not collide. Defaults to False." +msgstr "" + +#: 8aa56addc6ae4a7790dc957d1e1c2b60 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:20 of +msgid "Start the console command thread. Defaults to True." +msgstr "" + +#: 242566ff64584fba88e4fed8bb189110 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:28 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:23 +#: b6d5c80f7a8e4358889f1d90f9a579a4 of +msgid "" +"Worker slots for `submit_task` and threaded command handlers. Defaults to" +" 10." +msgstr "" + +#: 87d1cd7d6569498b8af8bc7064235784 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:26 of +msgid "" +"When True, do not call `start_TCP_Server`; the caller starts the server " +"when ready. Defaults to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:29 +#: ac43634ddb1a4d15a9f3d0f58d011441 of +msgid "" +"Negotiate the RSA-encrypted channel for every connection. Defaults to " +"True." +msgstr "" + +#: 1ffccca56902418ba98ee0c6776ad8fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:37 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:32 +#: e0eb36563dc5479a92ce7555eb5a7965 of +msgid "" +"``[pub_key_path, pvt_key_path]`` pair used instead of the default key " +"lookup; an invalid pair is ignored." +msgstr "" + +#: 420bbdf8d6bb400fbd3548bea03354ac +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:35 of +msgid "" +"Buffering ceiling in MiB for the in-memory forward pump; past it the " +"uploader is told to pause. Defaults to 2048." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 012113b84d6e439db2b772b810838e0a 57443c71b0b84d20b3cf755ee7118632 +#: 63ab5a2f2327475ba324e245f8e6e2ac 7cfe1a15e78d4c2d91a46599dc106667 +#: 8b33b297e4e64f2ba7463207ef17cc68 a4cb07de3c4d4a5c959c403938ed51e6 +#: a85e930c77364c9eaac1c8c9e9e2c740 c79723467b3a4fa1a7ddcb1038fd8414 +msgid "Raises" +msgstr "" + +#: 37d7580e55904c278f12fe0f31fe0d3d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:46 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:39 +#: ed59ec69b9a1430597e6d8758051aff5 of +msgid "" +"If the ``.Flow`` directories or ``decode_command_table.json`` cannot " +"be created or read." +msgstr "" + +#: 810bc26336b24ad1acba3f56d466f8e9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:1 of +msgid "Reserve this server's port range under the cross-process lock." +msgstr "" + +#: 5540b6efb5864c188e080ec7aad5c3ca 9e68e81f7a184af8a8fe90557dbaa5de +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:3 of +msgid "No-op unless ``is_hand_alloc_port`` is True." +msgstr "" + +#: 356ed2e8333d49b98ed6834612b4b56d 4a28dc37e6b74154b3790646ec8abfa7 +#: 5e267cb8af8d49e38ddaeabd8a7e9b57 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:6 +#: daf116d5b2264b1a88c836c1ad5192a3 of +msgid "Step between candidate ports." +msgstr "" + +#: 28bfef7616a1498db2b8827758175f68 7e663e1ae24640d1b856b1f9a0525380 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:8 +#: e18848eedc5d4d8abd97d73a0984a5d3 f81918ab77304aeb99302710e3062db2 of +msgid "Number of ports per step." +msgstr "" + +#: 11e0523d1b194702a07a92748468da25 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:1 of +msgid "Release this server's reserved port range." +msgstr "" + +#: 801c063c5fa447bda1dbf7f5ccbc6a9a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the server port range for this process." +msgstr "" + +#: 672bb7a9496c43ac8b706a3d0062ddfd +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:1 +#: of +msgid "Report whether the server port range is reserved by some process." +msgstr "" + +#: 19c63440e9ff4dadb6a3614ef17ef055 39423e052b70403ba07a713aa9e3cb4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:3 +#: of +msgid "True while the lock file exists." +msgstr "" + +#: 5b6cf828ffb24e098a0c86a09818da73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_unlock:1 +#: of +msgid "Remove the lock file that reserves the server port range." +msgstr "" + +#: 479bdf5518dc4a029ecf47ad44bdcd5d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:1 of +msgid "Allocate the next free server port range and record it on disk." +msgstr "" + +#: 34c085a1bfca4dedafc749bfdcf14ad5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:3 of +msgid "" +"``port`` is moved past the ranges already recorded by other servers, so " +"the instance ends up with a range of its own." +msgstr "" + +#: 197b30e318d741c2b1f8afc925e91e5c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:11 of +msgid "If the server port info file cannot be read or written." +msgstr "" + +#: 1c97c8ef484b42ab86d36b3742bf3879 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_free_port:1 of +msgid "Drop this server's entry from the on-disk port range record." +msgstr "" + +#: 9fc07e857fa048e2956e64bf78f6e386 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:1 of +msgid "Allocate a transfer port, waiting until one is free." +msgstr "" + +#: 505d7b9fb1024bc8ba886e5b0a38fbf2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:3 of +msgid "" +"Allocated port, or 0 when allocation is disabled " +"(``is_hand_alloc_port`` False)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:5 +#: e85fdf7f7a594e0b9aed565b4a3ab42f of +msgid "Allocated port, or 0 when allocation is disabled" +msgstr "" + +#: 153357eb91794cb692467afe5e94b41c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:6 of +msgid "(``is_hand_alloc_port`` False)." +msgstr "" + +#: 26c5c3768e1446e78c9efc5f8038b23d 280a1814abb049cbb26998bd681609ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:1 of +msgid "Release a port obtained from `palloc`." +msgstr "" + +#: 0e0379de669843778f6827973c29372e 3ee789104c4749b1a5749730be83a046 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:3 of +msgid "Port to release." +msgstr "" + +#: 6a8d4efd9f9a45e399525e2f169035ee +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:1 +#: f9203c3ebcb24fd0b5110110b8693b9c of +msgid "Allocate the next port above the base, or the first free one in range." +msgstr "" + +#: 73e9e2ddd451402a93d10dbac6ac9374 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 9d96a242eab8433ca750d670a5f63b82 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:5 of +msgid "Allocated port; None when the upward range is exhausted; 0 when" +msgstr "" + +#: 7b05e18fccbe4ab081d3cd22d59a48e0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:6 +#: ebba1d1151cc4d5991f783986cd0f480 of +msgid "allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 5f15d2ac36eb47a2b23fb93de93755c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:1 +#: f2d9abe435cf49f6825bb72a68013934 of +msgid "Release a port obtained from `file_palloc` and step the cursor back." +msgstr "" + +#: 6c1fb07a212d4bf19717407b0c7184d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:3 +#: c933ad6d68ec4c5d9bb5afc88c386fdd c9edcb78694a444fb0f2cc5f72db01fa +#: e32e165133154b9791e35f117f8a85a9 of +msgid "Port to release. Ignored when allocation is disabled." +msgstr "" + +#: 1afd81c685234a20aa41c5cde93d327d 7f5db505a5fa4c03a56e9ac5ff6aba74 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:1 of +msgid "Allocate the next port below the base, or the first free one in range." +msgstr "" + +#: 3fc63f6d51aa4c0fb22fd0f945f9cfd7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 74a441ccfa5e421dadd215ef0725a25f 9b5089cb85494c069d408e344db75d9d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:5 of +msgid "Allocated port; None when the downward range is exhausted; 0 when" +msgstr "" + +#: 183b4335eebc41058759e30b97477018 9c4b9bddd98c463f9e140bd2d8425dad +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:1 of +msgid "Release a port obtained from `spy_palloc` and step the cursor back." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:1 +#: c66c76cab8564da4b6b6cb855fd63fd5 df9eb5ed5e844a6a8f00e77be5c032e3 of +msgid "Register a custom command handler." +msgstr "" + +#: 85787fb3d66340eb9920983dd95e03c6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:3 +#: cdec2f24d2dc47bc8c505b7472dc3bf7 of +msgid "" +"Command to intercept, e.g. \"/my_command\"; matched case-insensitively " +"against the first token." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:6 +#: c7c29a3836c9493e9c8804cc5896144a of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back to the sender as the response." +msgstr "" + +#: 7c64d0c0b9ac44b995811a0fdaa0dfaf +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:10 of +msgid "" +"\"server\" for commands arriving from clients, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 7d2a7d91ad9743b29110a43100444097 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:13 +#: a0c074a9da664c5eb5fa63deb6765c36 of +msgid "" +"Run the handler on the worker pool instead of the reader thread. Defaults" +" to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:17 +#: ce0ad78e00b24bb8b675ce8d715b8323 e6c616f887334cb6925c03de2ad6f64c of +msgid "" +"False when ``where_to_run`` is neither \"server\" nor \"client\"; the" +" handler is then not registered." +msgstr "" + +#: 3a440bf70b36447e82577535a85baca1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:19 +#: f125f1386b434f3a8199b1ceb8c44eca of +msgid "False when ``where_to_run`` is neither \"server\" nor" +msgstr "" + +#: 1a68dac068db434eb76e28015005f29c 7c7784e8bc9f4a76b12b6be2d3df9285 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:20 of +msgid "\"client\"; the handler is then not registered." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:1 +#: ef5d20f1a16945368383a8cdbc3daa19 of +msgid "Register ``listener(client_id, message)`` for every inbound plain message." +msgstr "" + +#: 12cdf8300e934ace8498d00d39501ba2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:3 of +msgid "" +"Plain messages are the lines received from clients that do not start with" +" ``/``; commands go through the registered command handlers instead." +msgstr "" + +#: 9ec5fafe8ded489785400e3e489b8573 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:6 of +msgid "" +"``listener(client_id, message)`` where ``client_id`` is the sender's " +"``\"ip:port\"``. It runs on the receive thread, so it must not block, and" +" exceptions raised inside it are swallowed." +msgstr "" + +#: 3a640e40aeeb43938e65b49a2cd3dbba +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:1 +#: edb8edc2eb3f433186b54807a8ac43ad of +msgid "Unregister a listener previously added by `add_message_listener`." +msgstr "" + +#: 0336bd7428fa4f688cce21bb6a4156fe 6fd7bb75a29e4973ac2bd05119255039 +#: 977594c4b1074688958a018c03d9ee5b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:3 +#: b59375740438476aa73d6f19c2240ce6 of +msgid "Listener to remove; an unknown one is ignored." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:1 +#: cf95477e5794404fbdf4e877a132ba1c of +msgid "" +"Register ``listener(client_id, full_path, name, size, command)`` per " +"saved file." +msgstr "" + +#: 9c57d09f6d3d437890ae3ad306071701 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:3 of +msgid "" +"Fired after a file uploaded by a client (a direct send, or a forwarded " +"file/folder item staged on the server) has been fully written to " +"``file_transfer_dir``." +msgstr "" + +#: 6c938b7c8be3464f9925527b6deb4e1d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:7 of +msgid "" +"``listener(client_id, full_path, name, size, command)``; ``client_id`` is" +" the uploader's ``\"ip:port\"`` and ``command`` the wire command that " +"triggered the transfer, so a listener can recognise protocol pushes such " +"as ``/crypto_pub_key``. It runs on the transfer thread, so it must not " +"block." +msgstr "" + +#: 549734db517346ec814fa83ec2f46f00 57b1e98125974187b986386da7991dd0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:1 of +msgid "Unregister a listener previously added by `add_file_listener`." +msgstr "" + +#: 58f1ccd078ea406dab00d6d4be886a76 7a6c25f715d14b0aaf4ec7009db9fc3c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:1 of +msgid "Run a callable on the instance's worker pool." +msgstr "" + +#: 6e05c7f46b244ae195c32ee52797fa37 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:3 +#: a51df9d8fc924d468d8f6d8a61dfff12 of +msgid "Callable to run." +msgstr "" + +#: 6f9eee14b6cb4daf82acfbba1e34e873 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:5 +#: e1c79706c9414895bdf7bf183bf5d41a of +msgid "Positional arguments forwarded to ``func``." +msgstr "" + +#: 9221436fa83a4831942cf5be4e37e2cb 993bdb8fbe3240d3b1032014576182fe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:7 of +msgid "Keyword arguments forwarded to ``func``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:10 +#: a6b26d6cf54d4fafac2950cbe33ba56f df538e45ae7f4bd19398fd3d063a4744 of +msgid "" +"Handle for the submitted call; its worker slot is released when the " +"call finishes." +msgstr "" + +#: 214ffa9d5cb242d1b7a5f76c0da16cd6 60f19983f7384aefbb6c97b84dd25f93 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:12 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:12 of +msgid "Handle for the submitted call; its worker" +msgstr "" + +#: 942e2e90fc904344b71b0c2d9c39c9f6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:13 +#: eadf6af0a0954798933dc427ba8e107e of +msgid "slot is released when the call finishes." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:1 +#: a2284b241a28498892e5a59158961b2e fece61298a354d6db119309b29c6751d of +msgid "Start a temporary listener for a side channel (not the main protocol)." +msgstr "" + +#: 01d42ba32c2b41f482dd077cfb950f9b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:3 +#: b7455f1055b7459ca8eee2b5052eb808 of +msgid "" +"``handler(client_socket, address)`` started in its own thread for every " +"accepted connection." +msgstr "" + +#: 5e1b84445ee34c9cac00e1a2c9ef297b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:6 +#: c76bc24facf8407babf9023ef19c3879 of +msgid "Port to bind; None allocates one with `palloc`." +msgstr "" + +#: 4855e3ca5a3a41dfa37eb32a8d6c8d9c 9c69c4dd1d904078af48c6308ba35895 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:8 of +msgid "Listen backlog. Defaults to 1." +msgstr "" + +#: 29148f4a3160460f8dc27fd1e64c0a31 47f98fd15e6144bd845d175cd039a6c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:11 of +msgid "" +"``(port, thread, stop_event)``; setting ``stop_event`` ends the loop," +" which closes the socket and frees the port." +msgstr "" + +#: 91f803f6aa944d07a57cff61d1071efe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:13 +#: ecf3d32c44db40a8a13545d623ffe2c9 of +msgid "``(port, thread, stop_event)``; setting ``stop_event`` ends the" +msgstr "" + +#: 6ca9f2a625634468a15171916ec6d6ea +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:14 +#: abce9546b7444c6383c9020c4133ec63 of +msgid "loop, which closes the socket and frees the port." +msgstr "" + +#: 04767fd6d11641ac88c41c78867609f5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:17 +#: a270cfabdac64296b8a33ad4d662896f of +msgid "If ``port`` is None and no port can be allocated." +msgstr "" + +#: 031d3e96394c454093561375a377dcce 7e534c863e4a4275adf9a3a93dd5cbc0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:1 of +msgid "Open a temporary outbound connection for a side channel." +msgstr "" + +#: 112fda54e1eb4981b4272df61c5416e2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:3 +#: f9f4af8e25654b4ca38700b9faf635ef of +msgid "Host to connect to." +msgstr "" + +#: 26edcce3a1c24d5d95ca0b2a86cdff36 59d3c414b4304673be5d3232d2291680 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:5 of +msgid "Port to connect to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:7 +#: c5766672f68f47c9908e68d2ed3be7f3 of +msgid "Local port to bind; None lets the OS choose." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:9 +#: bfc1b713e9fc4fe797e50c357f472e76 c0e20701686a4ea68dcce54795822508 of +msgid "``on_data(data, client_socket)`` called for every received chunk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:13 +#: c563d04fcd7a41f1a2aeace42974430a cbd0df6f67a64a5580227be38a10020a of +msgid "" +"``(client_socket, thread, stop_event)``; setting ``stop_event`` ends " +"the receiver thread." +msgstr "" + +#: 0e1b9679f3104a9caab601e6bc7c0905 4fbec161e0c84ef1ba95379314846158 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:16 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:15 of +msgid "``(client_socket, thread, stop_event)``; setting ``stop_event``" +msgstr "" + +#: 14528f9f11144ced9b5822d2640fe2a9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:16 +#: c76fce92c3f944c1857df89bbc134540 of +msgid "ends the receiver thread." +msgstr "" + +#: 5ea5fd66c3d3434292e4d8e1dd1da094 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:1 of +msgid "Send one message to every connected client." +msgstr "" + +#: 32945f04e0564f9d88495506f0ab4f90 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:3 of +msgid "Clients whose send fails are disconnected and removed from ``clients``." +msgstr "" + +#: 00de0b1f60b14189b1629f8146db68a6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:5 of +msgid "Payload passed to `send_message`." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:7 +#: a91c8f6921c7435e990516c2134efcf8 of +msgid "``(ip, port)`` to leave out, typically the client the message came from." +msgstr "" + +#: 9d57c62f1fd94c1dacfe32aa50cc9665 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:1 +#: of +msgid "Send the messages of a console line to the clients named in it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:3 +#: e7fbf4bbd84b4920a2b9591fabcaf2d9 of +msgid "" +"``/send_msg`` line as typed: message text followed by one or more ``(ip, " +"port)`` identifiers; each message is delivered to the identifiers that " +"follow it. Addresses that are not connected are skipped with a console " +"notice." +msgstr "" + +#: 9d4819fe4ca94a1b81c9e0423ff98212 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:1 of +msgid "Write one line to a client socket, encrypting when the channel is up." +msgstr "" + +#: 1c7b88e29a0143a39e13441769186ef4 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:3 of +msgid "Target connection." +msgstr "" + +#: 2d32d96ad70e41d893b15bfa90c0864b 6f8bd9adaf2b4c44a9446b049144fe21 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:5 of +msgid "" +"Payload; a str is stripped and newline terminated, bytes are sent as they" +" are." +msgstr "" + +#: 5c03983cb460497fb72f126c1d8a2bc1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:9 of +msgid "" +"True when the payload was written, False for an unsupported payload " +"type." +msgstr "" + +#: 0b11a149117c4a1fb6bcf2fba3d1a515 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:11 of +msgid "True when the payload was written, False for an unsupported" +msgstr "" + +#: 602950a9f5ad48ea92ab60aea3b26488 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:12 of +msgid "payload type." +msgstr "" + +#: 149f767d72dc4da7b1d88fed768d2474 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:15 of +msgid "If the server is not running or no socket was passed." +msgstr "" + +#: 07dac2d20d7041779c8b189d45ee2e64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:16 of +msgid "If the socket write fails (the original error is re-raised)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:1 +#: caf2377ee7f348ab8c95bf2a27c44be4 of +msgid "Read up to ``msg_length`` bytes from a client socket." +msgstr "" + +#: 214af2667d0f443eb16c0a4aaf723b2e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:3 +#: cf94d4a352744a61a1f781128fae9694 of +msgid "Connection to read from." +msgstr "" + +#: 2b0cd669c9994324be7570e59a04677d 7c3eb31687804ba5a59acc140ba50230 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:5 of +msgid "Maximum number of bytes to read." +msgstr "" + +#: 51eade0145d74a2bbfc51070bc779703 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:8 +#: ad9dfdd3ef56445181d7204b7b5bfa15 of +msgid "Received bytes, empty when the peer closed the connection." +msgstr "" + +#: 7103cfcec26c44999cb5464251ee461c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:1 of +msgid "Serve one accepted client until it disconnects." +msgstr "" + +#: 65f9de7f44774aa6aef81fcbe4dc1c64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:3 of +msgid "" +"Registers the client, greets it, announces the encryption mode and reads " +"lines until the peer closes: commands go to `handle_command`, plain " +"messages go to the message listeners and to ``messages_dict``. Runs in " +"its own thread; the client is removed from ``clients`` and the socket " +"closed when the read loop ends for any reason." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:9 +#: db3915757e2842c8afee97b2d471411e of +msgid "Accepted connection." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:11 +#: fae378e888be4821a14ed2483cfde1e0 of +msgid "Peer ``(ip, port)``; used as the client id and as the key in ``clients``." +msgstr "" + +#: 32886e2babcf4283924034c600453c4a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:1 of +msgid "Dispatch one command line received from a client." +msgstr "" + +#: 8972b8c4edf149fd9a4008832c85ed31 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:3 of +msgid "" +"Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, " +"``/crypto_mode``, ``/file``, ``/file_folder``, " +"``/server_file_transfer_port`` and the crypto exchange lines) are handled" +" here; any other name goes to the handlers registered for the \"server\" " +"side via `register_command`. An encryption-mode mismatch closes the " +"connection; an unknown command is only reported on the console." +msgstr "" + +#: 208294924333477a8e7d65aa3130777e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:10 of +msgid "Connection the line came from." +msgstr "" + +#: 306e5026cfc1441d8c754f0e02866cf3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:12 of +msgid "Peer ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:14 +#: a2a3a1fd57e14260a53675457fdff0b4 d87b7a9e04464b7ea4f979d46b42932b of +msgid "Line including its leading ``/``." +msgstr "" + +#: 8d727a2aaab14beaa1b6858b59b0821d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:17 of +msgid "" +"Response for that client, or None when no response is due (crypto " +"lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:19 +#: ed04a3bf285b4f57bb6a5de0c4f7284f of +msgid "Response for that client, or None when no response is due" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:20 +#: c4ca8e9c3e9941e1b33b5e0af2c13bab of +msgid "" +"(crypto lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:1 +#: a784dfdc5ded411d985426e36f6e5435 of +msgid "Send one plain message to a connected target, tagged with its origin." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:3 +#: b545977244dc402aa8586264d2d933f3 of +msgid "" +"Public API for forward extensions: the message is wrapped in a " +"``/send_msg_from `` envelope so the receiver can " +"attribute it to the originator (see `parse_forwarded_message`)." +msgstr "" + +#: 8ee5275fbf894581ade9e6f30d5d4aff +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:7 +#: ee97948978e64b099947a0716f27d581 of +msgid "Destination ``(ip, port)``." +msgstr "" + +#: 38ffd8a7079d4794811e3504c01052f8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:9 of +msgid "Payload to deliver." +msgstr "" + +#: 3b48d6e3b84b4dde9a09f3c2f07c8b61 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:15 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:13 +#: cf0f9590c44e40309471c70ebd2e433d e2207f0b24bf42c984505e0de8c8f8bf of +msgid "Originating client ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:14 +#: d2471a90e269464c977239f406051120 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the envelope was sent." +msgstr "" + +#: 1ae8a7060df7423895175f210441a399 320d99b8923f4eaa886ffe0a16bfc6d9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:24 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:16 of +msgid "False when ``target`` is not connected (a console notice is" +msgstr "" + +#: 1a97f59b270f404096d9c2b37ab09a8c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:17 of +msgid "printed); True when the envelope was sent." +msgstr "" + +#: 6baab06dc12e4ecc80d1a16c26c2a19a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:1 of +msgid "Build the tagged wire command that pushes one forwarded item." +msgstr "" + +#: 0c3f9dcbcf8f413fbd5befefb960c402 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:3 of +msgid "" +"Public API for forward extensions. The originator tuple sits before the " +"trailing transfer id, where the receiver's existing parsers ignore it and" +" `parse_forward_originator` recovers it for attribution." +msgstr "" + +#: 3cf58937a2d04172bf544119279d85c1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:7 +#: a5ccb57e63d144c2844910f8ecf2faa4 of +msgid "\"file\" or \"file_folder\"." +msgstr "" + +#: 621a93603c8549c0ba047bee6aae5a38 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:9 +#: a20ab65e584c4daf9a9153008b713ee1 of +msgid "Relative folder path (folders only)." +msgstr "" + +#: 410039575afd4797a0eb1be9098ccda5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:11 +#: f0f56fb642874c3889fb0dacfed4f4ef of +msgid "File or folder name." +msgstr "" + +#: 214d3e0a88fa41d8a02af0a5d09c5ac6 72d2b6fdd324482eaea9803def1fe2da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:15 of +msgid "Transfer id shared by the pushed item." +msgstr "" + +#: 0aa1bd5b053544e98ae4ab3ae1add5f7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:17 +#: e7826b64fe174bf0a6f7faa4df49f491 of +msgid "Receiver-side destination directory." +msgstr "" + +#: 93e666db45e745619b90c744e1646b73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:20 of +msgid "Command line to hand to `send_message`." +msgstr "" + +#: 56d8327bb7414a5e85292f8f2ace9d42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:1 of +msgid "Push one forwarded file or folder item to a connected target." +msgstr "" + +#: 3098afa7f4ac43a184d081dda8faa6ba +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:3 of +msgid "" +"Public API for forward extensions: sends the line built by " +"`forward_target_command`, which the receiver attributes with " +"`parse_forward_originator`." +msgstr "" + +#: 9b9634061040430484cd2596f43a8feb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:22 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:25 +#: b8bb8c432f7649d5a35480fec0509a51 of +msgid "printed); True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:1 +#: b03f4edeca5f4cc19f6d695aac3690de of +msgid "Bind the server socket, then accept clients until `stop` runs." +msgstr "" + +#: 7e027ea659ab48e19ea346cb284fe144 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:3 of +msgid "" +"Blocks the calling thread. A console command thread is started when " +"``is_input_command_in_console`` is True, every accepted connection gets " +"its own `handle_client` thread, and a client beyond ``max_clients`` is " +"refused with a message. Socket errors and the end of the accept loop both" +" end in `stop`." +msgstr "" + +#: 291f5a8cc4f648ee9910e5cd45074675 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:1 of +msgid "Read console commands until the server stops." +msgstr "" + +#: 1ba2e428d432496688ef23f1c07562da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:3 of +msgid "" +"Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, " +"``/file_folder``, ``/multiple_file_multiple_client``, " +"``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward " +"commands are client-only and are refused here. Any other name goes to the" +" handlers registered with ``where_to_run=\"client\"``. Ctrl-C and EOF " +"stop the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:1 +#: eb90f49c77e64006a0c9ae861ff849b1 of +msgid "Stop the server and release everything it owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:3 +#: a059b1e71106487999c83c2e2042f509 of +msgid "" +"Closes the server socket and every client connection, flushes the message" +" and event stores, releases the allocated port range and clears " +"``running``. Safe to call more than once." +msgstr "" + +#: 32b06ea315b3404caa3a05c4aed0ce64 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:1 of +msgid "" +"TCP client: connect to a server, dispatch commands, send and receive " +"messages." +msgstr "" + +#: 2b30f5c9ecd24fc89cdcd58000ee1e99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:3 of +msgid "" +"Lines received from the server go through `receive_messages`: a line " +"starting with ``/`` is handled by `handle_server_command` (protocol " +"commands plus the handlers registered for the \"server\" side), any other" +" line is a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``. With " +"``is_input_command_in_console`` the console thread `interactive_mode` " +"sends typed lines to the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:12 +#: b9532c86786c43d5aea58b24157fefed of +msgid "Server address this client connects to." +msgstr "" + +#: 2cfa1e3e1674407ba1e2683e76fef65d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:18 of +msgid "Server port this client connects to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:24 +#: af04fdee2c95431a9b0c17f96e50c18c of +msgid "Local address the socket binds to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:30 +#: b1d4b5606e1c48b6a915815527542758 of +msgid "Local port, None when the OS chose one." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:32 +#: d926b4f3eb4b40a4acb21f780a4dc227 of +msgid "int | None" +msgstr "" + +#: 500cfd177cf5453d887f6904b73b7851 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:36 of +msgid "True while the connection is up." +msgstr "" + +#: 40f2a9e1b730422db3dd58bc2a6046e3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:1 of +msgid "Create the client and, unless extended, connect and start reading." +msgstr "" + +#: 0552aa36b7b74474af8f43b380dbee54 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:3 of +msgid "Server address to connect to; required before `connect` is called." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:6 +#: d246ff9aaf154bfa91b5c4c9d293afd5 of +msgid "Local address the socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:9 +#: e981b68ebbea46e9b556b09f4b5cd44a of +msgid "Server port. Defaults to 65432." +msgstr "" + +#: 766bf4c138234fe081bbd2668dcb2a5d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:11 of +msgid "Local port to bind; None lets the OS choose an ephemeral port." +msgstr "" + +#: 5d661c0f91f64ef39cacb1a5466d752c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:14 of +msgid "" +"Socket timeout in seconds for connect and receive. Must be None when " +"``is_wait_server`` is True." +msgstr "" + +#: 9478d02062284ecaa7838f931ab80780 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:17 of +msgid "Step between candidate ports in the allocation range. Defaults to 1." +msgstr "" + +#: 2e1695ae510b43758793257124fd23c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:22 of +msgid "Enter interactive mode after connecting. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:25 +#: e02da86957dd4f45bf84ede4e9c82cda of +msgid "Keep retrying while the server is not reachable. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:31 +#: e1fb7f67e1714a61a9e8fe500555c700 of +msgid "" +"When True, do not call `start_TCP_client`; the caller connects when " +"ready. Defaults to False." +msgstr "" + +#: 1634dbe84a95421a92a660f7c6fdd10e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:34 of +msgid "Negotiate the RSA-encrypted channel with the server. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:40 +#: cff66a7ac1ad410f96ed1ee03cb1cf53 of +msgid "" +"Buffer ceiling in MiB, kept for parity with the server class; the " +"client's forward path does not read it today. Defaults to 2048." +msgstr "" + +#: 0f06ff3b6db940da96ec09bee3dba526 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:45 of +msgid "If ``is_wait_server`` is True and ``timeout`` is not None." +msgstr "" + +#: 5dd677b9627d4b84b53dc7a6b2848b3f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:6 of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back as the response." +msgstr "" + +#: 0a57a7875bb94767904ff8d93fa77eb8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:10 of +msgid "" +"\"server\" for commands pushed by the server, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 876e8fe2a6d645829ba12ddbad5006df +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:1 of +msgid "Register ``listener(sender_id, message)`` for every inbound plain message." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:3 +#: d166a2cc8ea04bed82c5f6dd61dcdcf6 of +msgid "Mirrors the server-side contract; commands are not reported here." +msgstr "" + +#: 412834182c9442adaf0b85e24c200ccc +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:5 of +msgid "" +"``listener(sender_id, message)``; ``sender_id`` is the author's " +"``\"ip:port\"`` — the forwarding client for a message another client " +"forwarded here (``/send_msg_from`` envelope), or None for a direct push " +"from the server, which names no client author. It runs on the receive " +"thread, so it must not block, and exceptions raised inside it are " +"swallowed." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:1 +#: fc8a83e0b5f84273a8287b122c1a9a3e of +msgid "" +"Register ``listener(full_path, name, size, command)`` per saved inbound " +"file." +msgstr "" + +#: 34e3386853564aab9a824e96438fc3e5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:3 of +msgid "" +"Fired after a file pushed by the server (a direct send, or a forwarded " +"file/folder item) has been fully written to ``file_transfer_dir``." +msgstr "" + +#: 63f925709b83433e967610672b6fbc79 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:6 of +msgid "" +"``listener(full_path, name, size, command)``; ``command`` is the wire " +"command that triggered the transfer, so a listener can recognise protocol" +" pushes such as ``/crypto_pub_key``. It runs on the transfer thread, so " +"it must not block." +msgstr "" + +#: 0e91f82d8856443497c399f97643b757 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:1 of +msgid "Reserve this client's port range under the cross-process lock." +msgstr "" + +#: 6029a28126344e45b24394cb56b2f8f8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:3 of +msgid "No-op until the server assigns a range (see ``/client_alloc_port_range``)." +msgstr "" + +#: 32bed401c3574959ba5ed08fc9078401 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:1 of +msgid "Release this client's reserved port range." +msgstr "" + +#: 9f86b7aac4f44886be9cd87b56072b63 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:3 of +msgid "No-op unless a range was assigned (``is_hand_alloc_port`` True)." +msgstr "" + +#: 8154f96d9ec74bd588971a806d42f8c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the client port range for this process." +msgstr "" + +#: 69c2dd5d5d1d497c8bac1e62ab4fc30a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:1 +#: of +msgid "Report whether the client port range is reserved by some process." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_unlock:1 +#: c88f37ef2d5c47bda346e804508d6ec0 of +msgid "Remove the lock file that reserves the client port range." +msgstr "" + +#: 478443e187814a6ca7ad798277db023f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:1 of +msgid "Allocate the next free client port range and record it on disk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:3 +#: a8736540f4c3402fad9508e619d559d7 of +msgid "" +"``port`` is moved past the ranges already recorded by other clients on " +"this host, so each instance ends up with a range of its own." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:11 +#: bc44b1604e02467280b59cb8ad63d0af of +msgid "If the client port info file cannot be read or written." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_free_port:1 +#: e48dff576a074109a135db168b5dbe98 of +msgid "Drop this client's entry from the on-disk port range record." +msgstr "" + +#: 3aa62e8843c6495c862f43fc6dda9d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:1 of +msgid "Allocate a port, waiting until one is free." +msgstr "" + +#: 2f7752fa51d946b1a4a566b38f046e80 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:3 of +msgid "Allocated port, or 0 when no allocation range was assigned." +msgstr "" + +#: 503ab40fc32c4d548f65fa3272b2f4d2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:5 +#: b9fd549c95f24c2080d87cec92514db6 of +msgid "Allocated port; None when the upward range is exhausted; 0 when no" +msgstr "" + +#: 151a65f1c1984e4196c750108d0611cb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:6 of +msgid "allocation range was assigned." +msgstr "" + +#: 0c6201bb2d7e45d3a4916d7e50a21e10 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: 76c766a9c61f4075a762ccf55038a65e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:6 of +msgid "no allocation range was assigned." +msgstr "" + +#: 8d500513e8854ba4bb91c8905a9adb73 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:7 of +msgid "Local port to bind; None allocates one with `palloc`." +msgstr "" + +#: 840360623c8a4c2bbafe8fe7bbf9209c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:1 of +msgid "Connect to the server and start reading from it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:3 +#: df94288e660b48f6809358b3dc234ec9 of +msgid "" +"Binds ``client_port`` when one was configured, then retries while " +"``is_wait_server`` is True and the server is not reachable yet. Once the " +"socket is up the receive thread is started and the encryption mode is " +"negotiated, which closes the connection when the two sides disagree." +msgstr "" + +#: 22fccba0ab36499a9a7f4427abe5e9f4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:8 of +msgid "" +"True when the connection is established (and, if encryption is " +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 0270d0a2d42740a9889106339d9f2dbb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:10 of +msgid "True when the connection is established (and, if encryption is" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:11 +#: c194691cedfb48c6bfd97d9bdc6f2246 of +msgid "" +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 4dc38e3ef3b74b8e8c27378242a1925a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:1 of +msgid "Read from the server until the connection ends." +msgstr "" + +#: 431649a0d703476dadc6afa8adbe53d4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:3 of +msgid "" +"Runs on the receive thread: plain lines are reported to the message " +"listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes " +"are attributed to their sender first), other ``/`` lines go to " +"`handle_server_command`. Any end of the connection clears ``running`` and" +" releases the port range." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:1 +#: cf284b59f7f849039404ced20f8e12db of +msgid "Write one line to a socket, encrypting when the channel is up." +msgstr "" + +#: 40ba2ede2d3f4666b82107af129d24d0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:3 of +msgid "Target connection; the client passes ``self.client_socket``." +msgstr "" + +#: 2e663941eaf142cb96738add05f9ef4d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:10 of +msgid "" +"True when the payload was written; False when the client is not " +"running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: 38905ec7539d42e0839907c32dd8667f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:12 of +msgid "True when the payload was written; False when the client is not" +msgstr "" + +#: 3f7e2e6fedda476f996132c3f677f2f0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:13 of +msgid "running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:1 +#: a9220a130e694e0aba1d3b6320dff498 of +msgid "Send the payload of a console line to the server." +msgstr "" + +#: 2e208e83a89e4dac9793460a8808a042 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:3 of +msgid "" +"Console line such as ``/send_msg hello``; the first token (the command " +"name) is dropped and the second one is sent." +msgstr "" + +#: 39c8e70ca92b43b8ac687bc0aa7f4073 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:7 of +msgid "If the line has fewer than two tokens." +msgstr "" + +#: 8d47f30ae7b0451f8ecfe207ec835ff3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:1 of +msgid "Read up to ``msg_length`` bytes from a socket." +msgstr "" + +#: 10a39005f45c46728ef9a000eeaf9109 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:1 of +msgid "Dispatch one command line pushed by the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:3 +#: a7e781fde939475cb081204adac16ded of +msgid "" +"Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the" +" connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange " +"lines, and the transfer lines ``/file``, ``/file_folder``, " +"``/forward_upload``, ``/pause_trans``, ``/start_trans``, " +"``/forward_error``. Any other name goes to the handlers registered for " +"the \"server\" side via `register_command`; an unknown command is only " +"reported on the console." +msgstr "" + +#: 91942d88960c4793b5eab8c3654c1400 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:1 of +msgid "Forward plain messages to other connected clients through the server." +msgstr "" + +#: 75bb9147a7464a278e19af9d03b04ecb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:3 of +msgid "" +"The console command ``/forward_send_msg`` uses this; the client must be " +"connected. The server wraps each message in a ``/send_msg_from`` envelope" +" so the receiving client can attribute it back to this one." +msgstr "" + +#: 0f3c201936df45b687dc5532531b4a4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:7 of +msgid "Message texts to forward." +msgstr "" + +#: 277bbcb2143943b5b983badb00c3f4fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:9 of +msgid "Destination ``(ip, port)`` tuples." +msgstr "" + +#: 5ea8eea489fc43108a439d54fba31068 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:12 of +msgid "" +"True when the request was written to the server; False when the " +"client is not connected." +msgstr "" + +#: 2bc5d7e5dfa94eb29b49f12562f57c88 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:14 of +msgid "True when the request was written to the server; False when the" +msgstr "" + +#: 45d974ad946b408b91d41bcce65abcc8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:15 of +msgid "client is not connected." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:1 +#: c9401da7c37e40e5978c80e4876b11a0 of +msgid "Read console lines and act on them until the client stops." +msgstr "" + +#: 793bcfd02c014200b0ec47f8e15b4d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:3 of +msgid "" +"``/quit`` closes the connection; ``/send_msg``, ``/file``, " +"``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, " +"``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are " +"handled locally; any other name goes to the handlers registered with " +"``where_to_run=\"client\"``, and anything left is sent to the server as " +"it stands. Ctrl-C and EOF close the connection." +msgstr "" + +#: 6dbae803a56c442588b28af664b7a0e9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_file_console:1 of +msgid "" +"/forward_file ... ... [dest] (client " +"only)." +msgstr "" + +#: 05cad8b03e0747dc804ffbfda3122fe1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_folder_console:1 of +msgid "" +"/forward_folder ... ... [dest] " +"(client only)." +msgstr "" + +#: 6e9a9717a782419baecca63fd1f46baf +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:1 of +msgid "Close the connection and release everything the client owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:3 +#: ba6c5badecf6465db6036735870ed305 of +msgid "" +"Stops the receive loop, releases the port range, flushes the message and " +"event stores and closes the socket. Safe to call more than once." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:1 +#: c91a3245b93f46abbb49dbef7bcfeb1c of +msgid "Connect to the server and start the client loop." +msgstr "" + +#: 0c83552867484ea780fcff8d34e5c2d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:3 of +msgid "" +"Enters `interactive_mode` when ``is_input_command_in_console`` is True, " +"otherwise keeps the process alive while the connection is up. Exits the " +"process with status 1 when the connection cannot be established; Ctrl-C " +"and the end of the connection both run `close`." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po new file mode 100644 index 0000000..c97464c --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_udp.rst:2 +#: c777b065237a4eb1993c041388639a1d +msgid "PyFlow.network\\_api.connect\\_udp module" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.po new file mode 100644 index 0000000..17e8b88 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rst:2 9abd125493904de2bb64c9158a11243f +msgid "PyFlow.network\\_api package" +msgstr "" + +#: ../../api/PyFlow.network_api.rst:10 7012029060714904ba5d281c1d607be9 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po new file mode 100644 index 0000000..139c4a2 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po @@ -0,0 +1,232 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rsa_crypto.rst:2 +#: def9740b5a2e4aaeb2aeed8b4f02c0ec +msgid "PyFlow.network\\_api.rsa\\_crypto module" +msgstr "" + +#: 46d3090efafd4504870cf72a4bea22ab PyFlow.network_api.rsa_crypto:1 of +msgid "crypto_api (C/OpenSSL) RSA integration for PyFlow's TCP layer." +msgstr "" + +#: 5b1ff4a00b0f46258cda2e7440368de1 PyFlow.network_api.rsa_crypto:3 of +msgid "" +"A thin ctypes binding to the shared ``libcrypto_api`` plus the key " +"lifecycle required by the encrypted TCP channel:" +msgstr "" + +#: PyFlow.network_api.rsa_crypto:6 a5a0d6d600604683990a92555a49a3fe of +msgid "" +"Reuse an existing RSA keypair from ``~/.ssh`` (PEM private key) when one " +"is present and parseable, otherwise generate a fresh keypair into " +"``.Flow/pvt_key``. A caller-supplied keypair (``custom_keys``) is " +"honoured when both files parse and the pair matches." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:10 bb413dc908624589a3e57f8a2c702728 of +msgid "" +"Anti-MITM identity check (TOFU): every connection exchanges public keys " +"in plaintext. Each side records the peer in " +"``.Flow/pub_key/pub_key.json`` under the peer's ``(ip, port)`` with the " +"SHA-256 of its public key; a later connection from the same endpoint " +"presenting a different key is rejected, and a known key seen from a new " +"endpoint is re-registered under the new ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:16 aec1a7998276428881efcfac6fe4cebe of +msgid "" +"RSA-OAEP encrypt/decrypt with the ``_VALID`` plaintext signature so a " +"stale key (for example a rotated ``~/.ssh`` pair) is detected and the " +"peers re-exchange their public keys." +msgstr "" + +#: 60edbdead17c422782e688ab68f8a6d1 PyFlow.network_api.rsa_crypto:20 of +msgid "" +"The C library must be built first (``cmake -S . -B build && cmake --build" +" build``); see ``load_library`` for the search paths." +msgstr "" + +#: 67af7d5c5f41474fba915352e506a8b4 +#: PyFlow.network_api.rsa_crypto.CryptoLibraryError:1 of +msgid "Raised when the shared libcrypto_api cannot be loaded." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaKey:1 e620122ff0bc48dd985c4b6a0b3b41ce of +msgid "Owns an ``pf_rsa_key_t*`` handle; frees it on GC." +msgstr "" + +#: 1a7d8e9db5a3435486d716b13f2a2a56 +#: PyFlow.network_api.rsa_crypto.load_library:1 of +msgid "Locate and load the shared crypto_api library (cached)." +msgstr "" + +#: 670f4a719bf24c24b48ecb5d73b373ce +#: PyFlow.network_api.rsa_crypto.get_local_mac:1 of +msgid "Return a stable 48-bit machine identifier as colon-separated hex." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.get_local_mac:3 +#: f1306bb875d34b558f7418278fded413 of +msgid "" +"Uses ``uuid.getnode()`` (the real hardware MAC when one is available). " +"Server and client on the same host share this value; the ``_`` " +"prefix in the key file names keeps them apart." +msgstr "" + +#: 333763d798c54d52a9a56a3e4e3e2155 PyFlow.network_api.rsa_crypto.RsaCrypto:1 +#: of +msgid "Key lifecycle plus RSA-OAEP encrypt/decrypt for one role." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto:3 f52515121ac74575aaa36d45dd0341a2 +#: of +msgid "" +"``role`` is ``\"server\"`` or ``\"client\"`` and is used to name the " +"locally generated keypair (``pvt_key/_priv.pem``) and the peer key " +"cache (``pub_key/__.pem``). Peer identity is tracked" +" in ``pub_key/pub_key.json`` (TOFU, see ``verify_peer_pub``)." +msgstr "" + +#: 56fed5bcfe194b7f90a0c662ae36d6c0 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:1 of +msgid "Create the crypto wrapper for ``role`` (\"server\" or \"client\")." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:3 +#: ca4e27664ccf40ce935d991db43f7c69 of +msgid "" +"``custom_keys`` may be a ``[pub_key_path, pvt_key_path]`` pair to use a " +"user-supplied RSA keypair instead of the default lookup (``~/.ssh`` / " +"generated). The pair is validated on first use (paths exist, files parse," +" the keys match); an invalid pair is ignored and the default lookup is " +"used instead." +msgstr "" + +#: 7d89122ff0f54090b67209e3b08ae29c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:1 of +msgid "Load the RSA keypair (see module docstring) and cache handles." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:3 +#: d5e494e00be44891b33cb8e0ecfb8081 of +msgid "" +"Runs under ``_key_lock``: the private-key handle must never be replaced " +"(or freed on GC) while another thread is decrypting." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.reload_own_key:1 +#: f6f7d6ec8837464dad2dc647e6141a99 of +msgid "Re-read the private key (e.g. after a ~/.ssh rotation)." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:1 +#: e95d18da34d14376987a62f6cbbec778 of +msgid "" +"Path of the exchanged public key file for ``peer_role`` at ``(peer_ip, " +"peer_port)``." +msgstr "" + +#: 52dc6fb74f0c4175bfa2a2acc23cda0d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:4 of +msgid "" +"The IP is sanitized for the filesystem (``:`` -> ``_`` so IPv6 literals " +"are safe on every platform, including Windows)." +msgstr "" + +#: 731d92a1b64342dfbf7c467af0a4c00e +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:1 of +msgid "TOFU check-and-record for a peer public key." +msgstr "" + +#: 634283c14fb44424bf148a34d156fb71 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:3 of +msgid "" +"``peer_pem`` is the PEM text received on this connection, ``(peer_ip, " +"peer_port)`` the endpoint it came from. Returns ``(ok, reason)``:" +msgstr "" + +#: 6bb58c88f8454d7daca92b5a478ae788 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:7 of +msgid "" +"key already registered under any endpoint -> accept, and re-register it " +"under the current endpoint when it moved (IPs are dynamic and ports are " +"user-changeable);" +msgstr "" + +#: 74e2bcc89bae4fe5aecf99e6208e21d1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:10 of +msgid "" +"key unknown but the endpoint already holds a *different* key -> reject (a" +" trusted endpoint suddenly presenting a new key);" +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:12 +#: f856b875adc34f4a80c2b21cd506b143 of +msgid "" +"key and endpoint both unknown -> accept and record (first connection is " +"trusted, TOFU)." +msgstr "" + +#: 556fdf46d6c543e0ab06e2d4fabad7f1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:1 of +msgid "Move a freshly received public key file into the key cache." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:3 +#: a47e84f5d21949bf98e24834f18bed76 of +msgid "" +"Idempotent under concurrency: several transfers may deliver the same peer" +" key at once (multi-connection handshakes, several client processes " +"sharing one ``received_files/`` directory); if the source is already gone" +" because a concurrent store moved it, success is assumed when the " +"destination is in place." +msgstr "" + +#: 135944d28da54948862e515eb828072d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:1 of +msgid "Encrypt ``plaintext`` with the peer's public key file." +msgstr "" + +#: 4b4dded0ad904ce2bb87c9a43c8f87e5 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:3 of +msgid "" +"Returns the ASCII wire body (no trailing newline): each chunk is RSA-OAEP" +" encrypted and base64 encoded, chunks joined with ``|``. Raises if no " +"peer key is stored at ``peer_pem_path`` yet. The whole encryption runs " +"under ``_peer_pub_cache_lock`` so the peer handle cannot be freed mid-" +"encrypt (no-GIL safe)." +msgstr "" + +#: 8f1687f4ab984c029859fd1f9cfb968c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:1 of +msgid "Decrypt a wire body with our private key." +msgstr "" + +#: 8a1d1504bf4248fa9e1288776d19810b +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:3 of +msgid "" +"Returns ``(True, plaintext)`` on success, or ``(False, None)`` when the " +"key is stale/wrong or the ``_VALID`` signature is missing. Runs under " +"``_key_lock`` so the handle cannot be freed by a concurrent " +"``reload_own_key`` (no-GIL safe)." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.po new file mode 100644 index 0000000..305c591 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.rst:2 738ded08e11742acb4854652f885aa13 +msgid "PyFlow package" +msgstr "" + +#: ../../api/PyFlow.rst:10 8a3a28b0487c4bacb81f0afaa1b2902e +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.rst:19 28ca6a63167d49798f21d2796d6acf1e +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.po new file mode 100644 index 0000000..8020567 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.rst:2 e8518825f60e48f391b84d3fb415bb36 +msgid "PyFlow.transfer\\_web package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:10 8d8e622c8bdd4618b0e33d94e1000e42 +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:19 541ef21ed94743f2b9e11aadbde918b4 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po new file mode 100644 index 0000000..60918e1 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_client.rst:2 +#: 09f60d1b11ac4e92b480fa6284489970 +msgid "PyFlow.transfer\\_web.setup\\_client module" +msgstr "" + +#: 2f786e174934474a93c6498878a68612 PyFlow.transfer_web.setup_client:1 of +msgid "PyFlow TCP client web launcher." +msgstr "" + +#: 3e6b6a90f3e7484f8a0ca766f8305082 PyFlow.transfer_web.setup_client:3 of +msgid "" +"Starts a lightweight Flask backend on 127.0.0.1 and opens the connect UI " +"in the browser. The user enters the server address (an http/https domain" +" or a bare IP); the backend asks the server's web backend for the TCP " +"server address/port, starts the TCP client, and keeps the backend running" +" to relay the user's frontend actions." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po new file mode 100644 index 0000000..ba732c5 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_server.rst:2 +#: e6642bb55a204188b0abd0b07207713e +msgid "PyFlow.transfer\\_web.setup\\_server module" +msgstr "" + +#: 5966876a4564470ea674311ddb3ef63e PyFlow.transfer_web.setup_server:1 of +msgid "PyFlow TCP server web launcher." +msgstr "" + +#: 588fea0dc68a4a308f9a7cfb6fe5d819 PyFlow.transfer_web.setup_server:3 of +msgid "Checks ``transfer_web/.Flow_Web/setup_server.json``:" +msgstr "" + +#: PyFlow.transfer_web.setup_server:5 c3df4df524da40b18a58607efd9b5e4a of +msgid "" +"missing -> opens the server startup-configuration UI in the browser; the" +" UI saves the config (same shape as ``flow_setup``'s ``setup.json``) and " +"starts the TCP server class;" +msgstr "" + +#: 211850fce6d34d50ae152bce6d1aa3af PyFlow.transfer_web.setup_server:8 of +msgid "present -> starts the TCP server class directly from the saved config." +msgstr "" + +#: PyFlow.transfer_web.setup_server:10 cb510f0a77474eff8e8816fafc097343 of +msgid "" +"After the TCP server is up, the lightweight Flask backend serves the " +"status page and the client-facing API (``/api/server_info`` etc.) on the " +"server's address." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po new file mode 100644 index 0000000..f921302 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:2 +#: 76090548cb6a4066a3558553adad502a +msgid "PyFlow.transfer\\_web.web\\_backend package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:10 +#: 0d60b135791b45019583e72be4a02afc +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po new file mode 100644 index 0000000..6427508 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po @@ -0,0 +1,119 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.server_backend.rst:2 +#: 4e16e69f9ae9491a95bfcab9c70cfe24 +msgid "PyFlow.transfer\\_web.web\\_backend.server\\_backend module" +msgstr "" + +#: 15eb6d4006b3429a8fa230a2b460b1c5 +#: PyFlow.transfer_web.web_backend.server_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP server for the web tool." +msgstr "" + +#: 3c413c9e37f94c7485dba6d4f96b9bf2 +#: PyFlow.transfer_web.web_backend.server_backend:3 of +msgid "Two modes, one process:" +msgstr "" + +#: 557b793ac8e441db86cf59cad5e85371 +#: PyFlow.transfer_web.web_backend.server_backend:5 of +msgid "" +"``config`` mode: serves the server startup-configuration UI. The UI " +"shows every ``TCP_Server_Base`` parameter with its default value; on " +"submit the config is written to ``.Flow_Web/setup_server.json`` (same " +"shape as ``flow_setup``'s ``setup.json``) and the TCP server class is " +"started." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:10 +#: cb922669795b42c6ad5587b527c4356a of +msgid "" +"``status`` mode: serves the minimal status page plus the same " +"sidebar/input UI as the client frontend (forwarding disabled; native " +"sends to connected clients allowed). Also exposes the HTTP API that " +"clients use to discover the TCP server address/port." +msgstr "" + +#: 0c6784517a194d65926c71b7ce7f4836 +#: PyFlow.transfer_web.web_backend.server_backend:15 of +msgid "" +"The backend monitors ``server.clients``: whenever a client connects or " +"disconnects it broadcasts the current instance list to every connected " +"client (``/web_clients_update``), and it re-checks the list every minute." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:20 +#: e902a63f173f4e8ba20923c4d8f4b083 of +msgid "" +"Inbound events (plain-text messages and file uploads arriving from " +"clients) are captured on the TCP server's receive threads through " +"``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` " +"APIs, queued here, and polled by the frontend via ``/api/events``." +msgstr "" + +#: 30582f045fae468cb63a543324edfea8 +#: PyFlow.transfer_web.web_backend.server_backend:25 of +msgid "" +"Authentication: anonymous visitors get a white landing page (the server " +"addresses plus a login button); the configuration and status pages need a" +" session. Accounts live in ``.Flow_Web/users.json``; the first run seeds" +" the ``admin``/``admin`` administrator, and the frontend warns on every " +"login until those default credentials are changed." +msgstr "" + +#: 5fbb587ae4814cd683d5940abf4af37b +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:1 of +msgid "Account store backing the server web login (``.Flow_Web/users.json``)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:3 +#: f16349131c524eba9d2fd07591592afb of +msgid "" +"Passwords are PBKDF2-SHA256 records with a per-user salt. A missing " +"store file seeds the default ``admin``/``admin`` administrator; a store " +"file that exists but cannot be read is *not* re-seeded, so a damaged file" +" can never silently restore the default account." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.authenticate:1 +#: cfd5d429421942b3b0576566b76575a8 of +#, python-brace-format +msgid "Return ``{\"username\", \"role\"}`` for valid credentials, else ``None``." +msgstr "" + +#: 56e3012561ed4ed4aa77ffea3a744f93 +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.change_credentials:1 +#: of +msgid "Rename ``username`` and set its password (self-service)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp:1 +#: ab4c37912c524489a26700248a06704c of +msgid "Flask app + TCP_Server_Base wrapper for the web tool." +msgstr "" + +#: 7e707a76895243cbb7e49d4a943df5f2 +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_server.json`` and start the TCP server." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po new file mode 100644 index 0000000..46bddde --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.client_backend.rst:2 +#: a7e084a5059341aa9b7419059c4fa4b0 +msgid "PyFlow.transfer\\_web.web\\_front.client\\_backend module" +msgstr "" + +#: 22485674cb53445a86703fb118698523 +#: PyFlow.transfer_web.web_front.client_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP client for the web tool." +msgstr "" + +#: 31862d77d7184e55a1e729d7e72baed0 +#: PyFlow.transfer_web.web_front.client_backend:3 of +msgid "" +"The launcher (``setup_client.py``) starts this backend and opens the " +"connect UI in the browser. The user enters the server address (an " +"``http``/``https`` domain or a bare IP); the backend queries the server's" +" web backend ``/api/server_info`` for the TCP server address and port, " +"then starts the ``TCP_Client_Base`` instance. The backend stays up to " +"relay the user's frontend actions:" +msgstr "" + +#: 1a028258cda844ee945b9522d51afb9d +#: PyFlow.transfer_web.web_front.client_backend:10 of +msgid "messages/files/folders to the server use the native transfer methods;" +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:11 +#: e160c2a0daf9498cbe2041a8e964ff47 of +msgid "" +"messages to other clients use the native ``/forward_send_msg`` forwarding" +" (a client-only command relayed by the server);" +msgstr "" + +#: 16da18234f0a499893b711e7b555b42a +#: PyFlow.transfer_web.web_front.client_backend:13 of +msgid "" +"files/folders to other clients are forwarded through the built-in " +"``forward_extension_tcp`` extension." +msgstr "" + +#: 79dee862a03f4d0c9bc9403f8d461465 +#: PyFlow.transfer_web.web_front.client_backend:16 of +msgid "" +"The sidebar instance list is kept fresh by the server's " +"``/web_clients_update`` broadcasts; a reload button re-requests the list " +"via ``/web_sync_clients``." +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:20 +#: b952223fdafe4d52b8c34498aef1aeef of +msgid "" +"Inbound events (plain-text messages and files pushed by the server, " +"whether direct sends or client forwards) are captured on the TCP client's" +" receive threads through ``TCP_Client_Base``'s " +"``add_message_listener``/``add_file_listener`` APIs, queued here, and " +"polled by the frontend via ``/api/events``." +msgstr "" + +#: 0913e15917534119b775fb5c545c439d +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp:1 of +msgid "Flask app + TCP_Client_Base wrapper for the web tool." +msgstr "" + +#: 3e9a2f40d40b45869c4750ae3e542502 +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_client.json`` and start the TCP client." +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po new file mode 100644 index 0000000..cb72d88 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.rst:2 +#: 24ab9a394e604e0ea77050304ef77edd +msgid "PyFlow.transfer\\_web.web\\_front package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_front.rst:10 +#: ae412dcb5fd34608b462f44c2a8b9a17 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_CN/LC_MESSAGES/api/index.po b/docs/locale/zh_CN/LC_MESSAGES/api/index.po new file mode 100644 index 0000000..3920462 --- /dev/null +++ b/docs/locale/zh_CN/LC_MESSAGES/api/index.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_CN\n" +"Language-Team: zh_CN \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/index.rst:2 8d57b43b04b14ab8a07c05fb89785684 +msgid "API Reference" +msgstr "" + +#: ../../api/index.rst:4 2e33b7c5195747ebb1a15eb5e3e9c026 +msgid "" +"The pages below are generated from the code by ``sphinx-apidoc`` (see the" +" first line of ``docs/reBuild.sh``): each one pulls its text from the " +"docstrings at build time, so nothing here is written by hand." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/File_Transfer/File_Transfer.po b/docs/locale/zh_TW/LC_MESSAGES/File_Transfer/File_Transfer.po index 7ba5785..ae8588d 100644 --- a/docs/locale/zh_TW/LC_MESSAGES/File_Transfer/File_Transfer.po +++ b/docs/locale/zh_TW/LC_MESSAGES/File_Transfer/File_Transfer.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-02 13:19+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: zh_TW \n" @@ -644,11 +644,17 @@ msgid "Commands (client console only; rejected on the server console):" msgstr "客戶端控制台(接收者是伺服器):" #: ../../File_Transfer/File_Transfer.rst:413 14406316555743359a854540ab6937c8 -msgid "``/forward_file ... ...``" +#, fuzzy +msgid "" +"``/forward_file ... ... " +"[destination_file_path]``" msgstr "``/forward_file <檔案1> <檔案2> ... <位址1> <位址2> ...``" #: ../../File_Transfer/File_Transfer.rst:414 4dc576898177404da124649f7ce9ac28 -msgid "``/forward_folder ... ...``" +#, fuzzy +msgid "" +"``/forward_folder ... ... " +"[destination_file_path]``" msgstr "``/forward_folder <資料夾1> <資料夾2> ... <地址1> <地址2> ...``" #: ../../File_Transfer/File_Transfer.rst:416 220dfbf0a0d84559a8befb4eee500ba0 @@ -660,17 +666,31 @@ msgid "" msgstr "" "文件/資料夾的數量和目標客戶端的數量(寫為引號的地址元組)是無限的。無法到達(未連接到伺服器)或等於伺服器本身的目標位址將被跳過,而其餘目標仍將得到服務。" -#: ../../File_Transfer/File_Transfer.rst:424 f6697695e9ef4e3694fa9046e41578e0 +#: ../../File_Transfer/File_Transfer.rst:424 6137bf9406784c6f82d9f41560a52566 +msgid "" +"Like every transfer family, both commands accept an optional trailing " +"``destination_file_path`` that replaces the default save directory on every " +"receiving client: a forwarded file lands at ``/`` and" +" a forwarded folder keeps its structure under " +"``//...``. When the argument is omitted the " +"targets write to their default ``file_transfer_dir``." +msgstr "" +"像每個傳輸系列一樣,這兩個命令都接受可選的尾部`` destination_file_path `` " +",它取代了每個接收客戶端上的默認保存目錄:轉發的文件降落在``/`` " +",而轉發的文件夾將其結構保持在``//... ``之下。省略參數時,目標會寫入其預設的`` " +"file_transfer_dir ``。" + +#: ../../File_Transfer/File_Transfer.rst:435 f6697695e9ef4e3694fa9046e41578e0 msgid "The data path reuses the protocol's own transfer machinery:" msgstr "資料路徑重用協定自己的傳輸機制:" -#: ../../File_Transfer/File_Transfer.rst:427 6ee037f0e4024d2988fcb2ccf3deef70 +#: ../../File_Transfer/File_Transfer.rst:438 6ee037f0e4024d2988fcb2ccf3deef70 msgid "" "The forwarding client streams the file with the standard file-transfer byte " "stream (metadata header + 64 KiB chunks) to a transfer socket on the server." msgstr "轉送客戶端將具有標準檔案傳輸位元組流(元資料標頭 + 64 KiB 區塊)的檔案串流傳輸到伺服器上的傳輸套接字。" -#: ../../File_Transfer/File_Transfer.rst:432 43c94e746a5c46748846d0dc18a4e0ff +#: ../../File_Transfer/File_Transfer.rst:443 43c94e746a5c46748846d0dc18a4e0ff msgid "" "The server acts as a pure relay: it reads the stream into per-target memory " "queues and writes each chunk to every target's transfer socket. The server " @@ -679,7 +699,7 @@ msgid "" msgstr "" "伺服器充當純粹的中繼:它將流讀取到每個目標的記憶體佇列中,並將每個區塊寫入每個目標的傳輸套接字。伺服器從不解析超出大小標頭的檔案內容,也從不寫入磁碟。" -#: ../../File_Transfer/File_Transfer.rst:439 9dec57916af34e109bee7ce1e0bae2d4 +#: ../../File_Transfer/File_Transfer.rst:450 9dec57916af34e109bee7ce1e0bae2d4 msgid "" "Every target client receives the stream with the ordinary receive path " "(``file_transfer_mode_recv``) and writes it to its own local disk, exactly " @@ -687,11 +707,11 @@ msgid "" msgstr "" "每個目標客戶端都使用普通接收路徑(“file_transfer_mode_recv”)接收流並將其寫入自己的本機磁碟,就像伺服器直接推送檔案一樣。" -#: ../../File_Transfer/File_Transfer.rst:445 a095abaf821e41a6a66adbb776bc35ef +#: ../../File_Transfer/File_Transfer.rst:456 a095abaf821e41a6a66adbb776bc35ef msgid "### Memory Bounding and Flow Control" msgstr "### 記憶體限制與流量控制" -#: ../../File_Transfer/File_Transfer.rst:447 244214dd675345f8a72b1088ad99fea0 +#: ../../File_Transfer/File_Transfer.rst:458 244214dd675345f8a72b1088ad99fea0 msgid "" "Because uploader, server and targets may have different bandwidths, data can" " pile up in the server's memory. Both ``TCP_Server_Base`` and " @@ -710,25 +730,25 @@ msgstr "" "都採用 max_mem_buff 參數(以 MB 為單位,預設 2048,即 2 " "GB),此參數限制了轉送機制在該行程中可以保留的記憶體。當伺服器的緩衝位元組超過“max_mem_buff”時,它會向轉發客戶端發送“/pause_trans”,從而停止讀取來源檔案;一旦作者將緩衝區排空到低水位線(限制的一半)以下,伺服器就會發送“/start_trans”並恢復上傳。接收客戶端將每個區塊同步排出到磁碟,因此它們的緩衝記憶體保持受單一區塊的限制;兩側都存在“/pause_trans”/“/start_trans”處理程序,因此任何一側都可以在緩衝資料時限制傳輸。" -#: ../../File_Transfer/File_Transfer.rst:472 0aa14deaf6474437b421a65e21fce7d8 +#: ../../File_Transfer/File_Transfer.rst:483 0aa14deaf6474437b421a65e21fce7d8 msgid "Concurrency and Threading" msgstr "並發和線程" -#: ../../File_Transfer/File_Transfer.rst:474 825c5e1a8b9b405c967ca49a1861e258 +#: ../../File_Transfer/File_Transfer.rst:485 825c5e1a8b9b405c967ca49a1861e258 msgid "" "Both the server and the client use multiple levels of concurrency control to" " ensure stability during file transfers." msgstr "伺服器和用戶端均採用多層並發控制來確保文件傳輸過程中的穩定性。" -#: ../../File_Transfer/File_Transfer.rst:478 0d48b1245cd54597928cfc28d5b3c248 +#: ../../File_Transfer/File_Transfer.rst:489 0d48b1245cd54597928cfc28d5b3c248 msgid "### File Transfer Semaphore" msgstr "### 檔案傳輸信號量" -#: ../../File_Transfer/File_Transfer.rst:480 e2674ee9e0584513bc53aa815603fd24 +#: ../../File_Transfer/File_Transfer.rst:491 e2674ee9e0584513bc53aa815603fd24 msgid "Client: ``self.file_semaphore = threading.Semaphore(max_thread_num)``" msgstr "客戶端:``self.file_semaphore = threading.Semaphore(max_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:482 1c0d27d13ad844159d29c6e662d51d09 +#: ../../File_Transfer/File_Transfer.rst:493 1c0d27d13ad844159d29c6e662d51d09 msgid "" "Server: ``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" @@ -736,18 +756,18 @@ msgstr "" "伺服器:``self.file_semaphore = " "threading.Semaphore(max_file_transfer_thread_num)``" -#: ../../File_Transfer/File_Transfer.rst:485 5b0c8289b2c9460cb7305101226b66a7 +#: ../../File_Transfer/File_Transfer.rst:496 5b0c8289b2c9460cb7305101226b66a7 msgid "" "This semaphore limits the number of simultaneous file transfers (used " "primarily when sending folders or multiple files). Each transfer runs in its" " own thread, and the semaphore is acquired before the thread is started." msgstr "此信號量限制同時文件傳輸的數量(主要在發送資料夾或多個文件時使用)。每個傳輸都在自己的線程中運行,並且在線程啟動之前獲取信號量。" -#: ../../File_Transfer/File_Transfer.rst:492 6d93783b298e4ce98b999ff890d4a5f4 +#: ../../File_Transfer/File_Transfer.rst:503 6d93783b298e4ce98b999ff890d4a5f4 msgid "### Threading Model" msgstr "### 線程模型" -#: ../../File_Transfer/File_Transfer.rst:494 036a972600e645119d8a523fa08a52db +#: ../../File_Transfer/File_Transfer.rst:505 036a972600e645119d8a523fa08a52db msgid "" "Each file transfer runs in a dedicated daemon thread, created by the " "``_thread`` wrapper functions (e.g., " @@ -756,14 +776,14 @@ msgid "" msgstr "" "每個檔案傳輸都在專用的守護執行緒中執行,該執行緒由「_thread」包裝函數建立(例如「file_transfer_client_recv_client_start_thread」)。這可以防止緩慢的傳輸阻塞主控制循環。" -#: ../../File_Transfer/File_Transfer.rst:500 a84aaf5f9fb64d92b97c32185a1e7cb5 +#: ../../File_Transfer/File_Transfer.rst:511 a84aaf5f9fb64d92b97c32185a1e7cb5 msgid "" "The thread that receives the transfer command (e.g., the server's " "``handle_command`` thread) does not wait for the transfer to complete; it " "returns immediately after spawning the worker thread." msgstr "接收傳輸命令的線程(例如伺服器的“handle_command”線程)不會等待傳輸完成;它在產生工作線程後立即返回。" -#: ../../File_Transfer/File_Transfer.rst:505 669fb6da8e7444dd829a6e14295648b1 +#: ../../File_Transfer/File_Transfer.rst:516 669fb6da8e7444dd829a6e14295648b1 msgid "" "The low-level receive function (``file_transfer_mode_recv``) blocks while " "reading from the transfer socket, but because it runs in a dedicated thread," @@ -771,11 +791,11 @@ msgid "" msgstr "" "低階接收函數(“file_transfer_mode_recv”)在從傳輸套接字讀取時會阻塞,但由於它在專用執行緒中運行,因此主連接仍保持回應。" -#: ../../File_Transfer/File_Transfer.rst:511 e39f8ae06ae84a6b84de8a3008ca17cb +#: ../../File_Transfer/File_Transfer.rst:522 e39f8ae06ae84a6b84de8a3008ca17cb msgid "### Thread Pool for Custom Commands" msgstr "### 自訂指令的執行緒池" -#: ../../File_Transfer/File_Transfer.rst:513 3ad49853529d4563bacdfb4ca6eee115 +#: ../../File_Transfer/File_Transfer.rst:524 3ad49853529d4563bacdfb4ca6eee115 msgid "" "Both classes also provide a ``ThreadPoolExecutor`` " "(``self._custom_executor``) for custom command handlers. When a handler is " @@ -786,11 +806,11 @@ msgid "" msgstr "" "這兩個類別也為自訂命令處理程序提供了“ThreadPoolExecutor”(“self._custom_executor”)。當使用「run_in_thread=True」註冊處理程序時,它會透過「submit_task」提交到該池,該池也使用信號量將並發限制為「max_custom_workers」。此機制獨立於檔案傳輸信號量,旨在用於通用命令處理。" -#: ../../File_Transfer/File_Transfer.rst:528 1862d74da28a40219f82cfd7af89c126 +#: ../../File_Transfer/File_Transfer.rst:539 1862d74da28a40219f82cfd7af89c126 msgid "Port Allocation and Management" msgstr "連接埠分配與管理" -#: ../../File_Transfer/File_Transfer.rst:530 bd53ec8aa4ef498aa0815db16ed03140 +#: ../../File_Transfer/File_Transfer.rst:541 bd53ec8aa4ef498aa0815db16ed03140 msgid "" "File transfers require ephemeral ports for the secondary data connections. " "The ``palloc()`` and ``pfree()`` methods are used to obtain and release " @@ -798,7 +818,7 @@ msgid "" msgstr "" "檔案傳輸需要臨時連接埠來進行輔助資料連線。 ``palloc()`` 和 ``pfree()`` 方法用於取得和釋放這些連接埠。有兩種模式可供選擇:" -#: ../../File_Transfer/File_Transfer.rst:536 eeaa9379d7d941159ac34f30ff6bb5f9 +#: ../../File_Transfer/File_Transfer.rst:547 eeaa9379d7d941159ac34f30ff6bb5f9 msgid "" "**Automatic mode** (``is_hand_alloc_port=False``): ``palloc()`` returns " "``0``, and the operating system assigns a free port when the socket is " @@ -807,7 +827,7 @@ msgstr "" "**自動模式**(``is_hand_alloc_port=False``):``palloc()`` " "傳回``0``,作業系統在套接字綁定時指派一個空閒連接埠。這是大多數用例的推薦模式。" -#: ../../File_Transfer/File_Transfer.rst:541 80e08ac6614b4dcd88cd0e4d31a3da64 +#: ../../File_Transfer/File_Transfer.rst:552 80e08ac6614b4dcd88cd0e4d31a3da64 msgid "" "**Manual mode** (``is_hand_alloc_port=True``): Ports are drawn from a " "configurable range ``[self.min_port, self.max_port]`` with a step size " @@ -819,7 +839,7 @@ msgstr "" "self.max_port]`` " "中提取端口,步长为``port_add_step``。伺服器透過“/client_alloc_port_range”向客戶端廣播允許的範圍,然後客戶端使用相同的手動分配邏輯。" -#: ../../File_Transfer/File_Transfer.rst:551 29cfdab183eb4f9f83de41ef62a60785 +#: ../../File_Transfer/File_Transfer.rst:562 29cfdab183eb4f9f83de41ef62a60785 msgid "" "*Note: For more details about port allocation, please visit the Port " "Allocation API sections in :doc:`TCP_Server_APIs` and " @@ -827,61 +847,61 @@ msgid "" msgstr "" "*注意:有關連接埠分配的更多詳細信息,請訪問 TCP_Server_APIs 和 TCP_Client_APIs 中的連接埠分配 API 部分。 *" -#: ../../File_Transfer/File_Transfer.rst:559 189736d8d1f8464b8d301f219700fda4 +#: ../../File_Transfer/File_Transfer.rst:570 189736d8d1f8464b8d301f219700fda4 msgid "Error Handling and Timeouts" msgstr "錯誤處理和超時" -#: ../../File_Transfer/File_Transfer.rst:561 bcc373777989436f9df3657446d1a7f2 +#: ../../File_Transfer/File_Transfer.rst:572 bcc373777989436f9df3657446d1a7f2 msgid "### Timeout Values" msgstr "### 逾時值" -#: ../../File_Transfer/File_Transfer.rst:563 27d75d097a804ec28ebec5c67469c9c6 +#: ../../File_Transfer/File_Transfer.rst:574 27d75d097a804ec28ebec5c67469c9c6 msgid "" "**Start signal timeout**: 10 seconds. If the receiver does not send " "``server_start_file_transfer_sign`` within this time, the sender aborts." msgstr "" "**啟動訊號逾時**:10 秒。如果接收者在此時間內未傳送“server_start_file_transfer_sign”,則傳送者將中止。" -#: ../../File_Transfer/File_Transfer.rst:567 93e877c411eb4c96bd7861ff61f35e7a +#: ../../File_Transfer/File_Transfer.rst:578 93e877c411eb4c96bd7861ff61f35e7a msgid "" "**Port negotiation timeout**: 20 seconds. The initiator waits for the peer's" " ``/server_file_transfer_port`` response." msgstr "**連接埠協商逾時**:20 秒。發起者等待對等方的「/server_file_transfer_port」回應。" -#: ../../File_Transfer/File_Transfer.rst:570 9e1d395bb59540a49393bebe6630e5eb +#: ../../File_Transfer/File_Transfer.rst:581 9e1d395bb59540a49393bebe6630e5eb msgid "" "**Completion acknowledgement timeout**: ``30 + (file_size // (100 * 1024 * " "1024)) * 10`` seconds. Larger files get proportionally more time." msgstr "" "**完成確認逾時**:``30 + (file_size // (100 * 1024 * 1024)) * 10`` 秒。文件越大,相應的時間就越長。" -#: ../../File_Transfer/File_Transfer.rst:574 6b4e6135669b41178fdbb47ea5975381 +#: ../../File_Transfer/File_Transfer.rst:585 6b4e6135669b41178fdbb47ea5975381 msgid "### Error Signalling" msgstr "### 錯誤訊號" -#: ../../File_Transfer/File_Transfer.rst:576 448fdc6ba3dd4d9eae925e7e241f8cb8 +#: ../../File_Transfer/File_Transfer.rst:587 448fdc6ba3dd4d9eae925e7e241f8cb8 msgid "" "Any error during the handshake or data transfer causes the failing side to " "send ``error_sign`` over the transfer socket." msgstr "握手或資料傳輸期間的任何錯誤都會導致失敗方透過傳輸套接字發送“error_sign”。" -#: ../../File_Transfer/File_Transfer.rst:579 5b4c23a93e984ce0af2e43cf4752225b +#: ../../File_Transfer/File_Transfer.rst:590 5b4c23a93e984ce0af2e43cf4752225b msgid "" "The other side, upon receiving the error sign, closes the transfer socket " "and aborts the transfer." msgstr "另一端收到錯誤標誌後,關閉傳輸套接字並中止傳輸。" -#: ../../File_Transfer/File_Transfer.rst:582 b337a2726ded4d619c5e8026bef3f6ea +#: ../../File_Transfer/File_Transfer.rst:593 b337a2726ded4d619c5e8026bef3f6ea msgid "" "The main control connection remains unaffected; only the transfer socket is " "closed." msgstr "主控連線不受影響;僅關閉傳輸套接字。" -#: ../../File_Transfer/File_Transfer.rst:586 68be52c8e5ce447a9c5ec51a661229cf +#: ../../File_Transfer/File_Transfer.rst:597 68be52c8e5ce447a9c5ec51a661229cf msgid "### Exception Handling" msgstr "### 例外處理" -#: ../../File_Transfer/File_Transfer.rst:588 036db9bc1858417ca589816028b83f80 +#: ../../File_Transfer/File_Transfer.rst:599 036db9bc1858417ca589816028b83f80 msgid "" "All socket operations are wrapped in try-except blocks. When an exception " "occurs (e.g., connection reset, file not found), the error is logged with " @@ -892,11 +912,11 @@ msgstr "" "區塊中。當發生異常時(例如,連線重設、未找到檔案),錯誤將會以``traceback.print_exc()`` " "記錄,且傳輸會正常中止。如果可能,將發送“error_sign”,並關閉傳輸套接字。" -#: ../../File_Transfer/File_Transfer.rst:600 fe9377db9cf04b9da0dbe4d07c730adf +#: ../../File_Transfer/File_Transfer.rst:611 fe9377db9cf04b9da0dbe4d07c730adf msgid "Related API Definitions" msgstr "相關API定義" -#: ../../File_Transfer/File_Transfer.rst:602 cc71cb9b1d6642a2acc89d45a49022cc +#: ../../File_Transfer/File_Transfer.rst:613 cc71cb9b1d6642a2acc89d45a49022cc msgid "" "This section lists all public file-transfer related methods in " "``TCP_Server_Base`` and ``TCP_Client_Base``. For a complete list of all " @@ -905,11 +925,11 @@ msgstr "" "本節列出了「TCP_Server_Base」和「TCP_Client_Base」中所有與公共檔案傳輸相關的方法。有關所有公共 API " "的完整列表,請參閱本文檔末尾的表格。" -#: ../../File_Transfer/File_Transfer.rst:608 d6683e415f794c5bb693f8c24370e7f9 +#: ../../File_Transfer/File_Transfer.rst:619 d6683e415f794c5bb693f8c24370e7f9 msgid "### Server-Side File Transfer APIs" msgstr "### 伺服器端檔案傳輸 API" -#: ../../File_Transfer/File_Transfer.rst:618 9123c69197c34d93bb68d088897ebeca +#: ../../File_Transfer/File_Transfer.rst:629 9123c69197c34d93bb68d088897ebeca msgid "" "Initiates a server-to-client file transfer. ``message`` is the command " "string (e.g., ``/file /path/to/file.txt (127.0.0.1,54321)``). If " @@ -919,11 +939,11 @@ msgstr "" "啟動伺服器到客戶端的檔案傳輸。 ``message`` 是指令字串(例如,``/file /path/to/file.txt " "(127.0.0.1,54321)``)。如果提供了「file_folder_abspath」(用於資料夾傳輸),則它指定父資料夾的絕對路徑。" -#: ../../File_Transfer/File_Transfer.rst:633 367dfd82c95c413d963a15152469fc44 +#: ../../File_Transfer/File_Transfer.rst:644 367dfd82c95c413d963a15152469fc44 msgid "Thread-safe version that starts a new thread for the transfer." msgstr "線程安全版本,啟動新線程進行傳輸。" -#: ../../File_Transfer/File_Transfer.rst:642 d4aa76b7eb2c46c29adee0120a939b66 +#: ../../File_Transfer/File_Transfer.rst:653 d4aa76b7eb2c46c29adee0120a939b66 msgid "" "Sends an entire folder from server to client. ``message`` should be of the " "form ``/file_folder ``." @@ -931,7 +951,7 @@ msgstr "" "將整個資料夾從伺服器傳送到客戶端。 “message” 的格式應為“/file_folder " "”。" -#: ../../File_Transfer/File_Transfer.rst:652 fcb785ff144746fab81e95ec2ab056e1 +#: ../../File_Transfer/File_Transfer.rst:663 fcb785ff144746fab81e95ec2ab056e1 msgid "" "Sends multiple files to multiple clients. The message format is " "``/multiple_file_multiple_client ... " @@ -940,7 +960,7 @@ msgstr "" "將多個文件傳送給多個客戶端。訊息格式為``/multiple_file_multiple_client ... " " ...``。文件必須出現在客戶面前。" -#: ../../File_Transfer/File_Transfer.rst:664 9c6f024528f344e399b62023c7f8c858 +#: ../../File_Transfer/File_Transfer.rst:675 9c6f024528f344e399b62023c7f8c858 msgid "" "Sends different file lists to different clients. The message alternates " "between groups: a list of files, then a list of client addresses, then the " @@ -950,107 +970,107 @@ msgstr "" "向不同的客戶端發送不同的文件清單。訊息在群組之間交替:文件列表,然後是客戶端位址列表,然後是下一個文件列表,等等。範例:``/diff_multiple_file_diff_multiple_client" " a.txt b.txt (ip1,port1) (ip2,port2) c.txt (ip3,port3)``" -#: ../../File_Transfer/File_Transfer.rst:682 f8a880287b7b49d4bdc2239ecf4a0577 +#: ../../File_Transfer/File_Transfer.rst:693 f8a880287b7b49d4bdc2239ecf4a0577 msgid "" "Receives a file from a client. Called internally when the server receives a " "``/file`` command from a client." msgstr "從客戶端接收文件。當伺服器從客戶端接收到“/file”命令時在內部呼叫。" -#: ../../File_Transfer/File_Transfer.rst:698 97bfaa9c24ae47c39328707b8f17a91a +#: ../../File_Transfer/File_Transfer.rst:709 97bfaa9c24ae47c39328707b8f17a91a msgid "" "Low-level receive function that performs the handshake and writes the " "incoming file to disk." msgstr "執行握手並將傳入檔案寫入磁碟的低階接收函數。" -#: ../../File_Transfer/File_Transfer.rst:711 db6a97ca42ba43e59d5c20695039d4ee +#: ../../File_Transfer/File_Transfer.rst:722 db6a97ca42ba43e59d5c20695039d4ee msgid "" "Low-level send function that connects to the receiver and transmits the " "file." msgstr "連接到接收器並傳輸檔案的低階發送函數。" -#: ../../File_Transfer/File_Transfer.rst:713 3e7b689215e840bebd368b4d29104ebc +#: ../../File_Transfer/File_Transfer.rst:724 3e7b689215e840bebd368b4d29104ebc msgid "### Client-Side File Transfer APIs" msgstr "### 客戶端檔案傳輸 API" -#: ../../File_Transfer/File_Transfer.rst:723 1e97e492d6504579a9a265eb1242395e +#: ../../File_Transfer/File_Transfer.rst:734 1e97e492d6504579a9a265eb1242395e msgid "" "Initiates a client-to-server file transfer. ``message`` is the user command " "(e.g., ``/file mydoc.txt``). Used internally by the interactive console." msgstr "啟動客戶端到伺服器的檔案傳輸。 “message” 是使用者命令(例如“/file mydoc.txt”)。由互動式控制台內部使用。" -#: ../../File_Transfer/File_Transfer.rst:735 -#: ../../File_Transfer/File_Transfer.rst:786 0cd2695763114a0b831df0bfa80a3d56 +#: ../../File_Transfer/File_Transfer.rst:746 +#: ../../File_Transfer/File_Transfer.rst:797 0cd2695763114a0b831df0bfa80a3d56 msgid "Thread-safe version." msgstr "線程安全版本。" -#: ../../File_Transfer/File_Transfer.rst:744 261399ca508d463eafa7f03a00bfc658 +#: ../../File_Transfer/File_Transfer.rst:755 261399ca508d463eafa7f03a00bfc658 msgid "Sends a folder from client to server." msgstr "將資料夾從客戶端傳送到伺服器。" -#: ../../File_Transfer/File_Transfer.rst:753 ef5e3b92b11c4530960c1c344a51c73b +#: ../../File_Transfer/File_Transfer.rst:764 ef5e3b92b11c4530960c1c344a51c73b msgid "Sends multiple files from client to server." msgstr "將多個文件從客戶端傳送到伺服器。" -#: ../../File_Transfer/File_Transfer.rst:762 f2c90ee949d7484480cbb2cd5310bf26 +#: ../../File_Transfer/File_Transfer.rst:773 f2c90ee949d7484480cbb2cd5310bf26 msgid "Sends multiple folders from client to server." msgstr "將多個資料夾從客戶端傳送到伺服器。" -#: ../../File_Transfer/File_Transfer.rst:775 0b13d26a40174243a15698b4bfcbb69f +#: ../../File_Transfer/File_Transfer.rst:786 0b13d26a40174243a15698b4bfcbb69f msgid "" "Receives a file from the server (called when the server initiates a " "transfer)." msgstr "從伺服器接收檔案(在伺服器啟動傳輸時呼叫)。" -#: ../../File_Transfer/File_Transfer.rst:797 3b78a4355d7f4ee2bbbe6bf934a962c0 +#: ../../File_Transfer/File_Transfer.rst:808 3b78a4355d7f4ee2bbbe6bf934a962c0 msgid "Receives a folder from the server." msgstr "從伺服器接收資料夾。" -#: ../../File_Transfer/File_Transfer.rst:812 3d044e0754b94d1289b491502ce83610 +#: ../../File_Transfer/File_Transfer.rst:823 3d044e0754b94d1289b491502ce83610 msgid "Low-level receive function on the client side." msgstr "客戶端的低階接收函數。" -#: ../../File_Transfer/File_Transfer.rst:824 7311023a7fa644ed9b57a2873cd3bca8 +#: ../../File_Transfer/File_Transfer.rst:835 7311023a7fa644ed9b57a2873cd3bca8 msgid "" "Low‑level send function on the client side (identical to server's version)." msgstr "客戶端的低階發送功能(與伺服器版本相同)。" -#: ../../File_Transfer/File_Transfer.rst:829 7e74e9a09a8a4cf2a8a372a50b1ee51b +#: ../../File_Transfer/File_Transfer.rst:840 7e74e9a09a8a4cf2a8a372a50b1ee51b msgid "Public API Summary" msgstr "公共API摘要" -#: ../../File_Transfer/File_Transfer.rst:831 87aaf0a5d3b44f41a419d97b6567f6d0 +#: ../../File_Transfer/File_Transfer.rst:842 87aaf0a5d3b44f41a419d97b6567f6d0 msgid "" "All public APIs (including non-file-transfer methods) are listed below for " "reference." msgstr "下面列出了所有公共 API(包括非文件傳輸方法)以供參考。" -#: ../../File_Transfer/File_Transfer.rst:835 a195ee393f2a442c810e59811a6ae126 +#: ../../File_Transfer/File_Transfer.rst:846 a195ee393f2a442c810e59811a6ae126 msgid "### TCP_Server_Base Public APIs" msgstr "### TCP_Server_Base 公用 API" -#: ../../File_Transfer/File_Transfer.rst:837 0408d74a9140472e9a774143a60e5749 +#: ../../File_Transfer/File_Transfer.rst:848 0408d74a9140472e9a774143a60e5749 msgid "``file_transfer_server_recv_client_start``" msgstr "``file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:838 24a33cb212844e04a8a271ada32412f0 +#: ../../File_Transfer/File_Transfer.rst:849 24a33cb212844e04a8a271ada32412f0 msgid "``file_transfer_server_recv_client_start_thread``" msgstr "``file_transfer_server_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:839 7e286d1ece6940868bea6b937486e617 +#: ../../File_Transfer/File_Transfer.rst:850 7e286d1ece6940868bea6b937486e617 msgid "``folder_file_transfer_server_recv_client_start``" msgstr "``folder_file_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:840 37fc5bd346c84415bb15138a42508fbe +#: ../../File_Transfer/File_Transfer.rst:851 37fc5bd346c84415bb15138a42508fbe msgid "``multiple_file_multiple_client_transfer_server_recv_client_start``" msgstr "``multiple_file_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:841 38a2b16e3ab648688cf7e8d1cae8be72 +#: ../../File_Transfer/File_Transfer.rst:852 38a2b16e3ab648688cf7e8d1cae8be72 msgid "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" msgstr "" "``diff_multiple_file_diff_multiple_client_transfer_server_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:843 11476e0eaa3b4a4286612812e4e2c004 +#: ../../File_Transfer/File_Transfer.rst:854 11476e0eaa3b4a4286612812e4e2c004 msgid "" "(The low-level helpers ``file_transfer_server_recv_server_start``, " "``file_transfer_mode_recv``, and ``file_transfer_mode`` are not considered " @@ -1058,65 +1078,65 @@ msgid "" msgstr "" "(低階助手「file_transfer_server_recv_server_start」、「file_transfer_mode_recv」和「file_transfer_mode」不被認為是公共的,但為了完整性而被記錄下來。)" -#: ../../File_Transfer/File_Transfer.rst:849 907e861cebe648fbacb799bae8bb15e0 +#: ../../File_Transfer/File_Transfer.rst:860 907e861cebe648fbacb799bae8bb15e0 msgid "### TCP_Client_Base Public APIs" msgstr "### TCP_Client_Base 公用 API" -#: ../../File_Transfer/File_Transfer.rst:851 88bd80083f754caeb026f9ce1b8c6b55 +#: ../../File_Transfer/File_Transfer.rst:862 88bd80083f754caeb026f9ce1b8c6b55 msgid "``file_transfer_client_recv_client_start``" msgstr "``file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:852 3b21ff73da694465906482412b4fb4e3 +#: ../../File_Transfer/File_Transfer.rst:863 3b21ff73da694465906482412b4fb4e3 msgid "``file_transfer_client_recv_client_start_thread``" msgstr "``file_transfer_client_recv_client_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:853 c8af43896a2443a5b14bd29863731c2c +#: ../../File_Transfer/File_Transfer.rst:864 c8af43896a2443a5b14bd29863731c2c msgid "``folder_file_transfer_client_recv_client_start``" msgstr "``folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:854 7a3d3e4a52334b169a62d3b30d7a3190 +#: ../../File_Transfer/File_Transfer.rst:865 7a3d3e4a52334b169a62d3b30d7a3190 msgid "``multiple_file_transfer_client_recv_client_start``" msgstr "``multiple_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:855 19e18754c4674842ad5116f709588e03 +#: ../../File_Transfer/File_Transfer.rst:866 19e18754c4674842ad5116f709588e03 msgid "``multiple_folder_file_transfer_client_recv_client_start``" msgstr "``multiple_folder_file_transfer_client_recv_client_start``" -#: ../../File_Transfer/File_Transfer.rst:856 4cf80f9f34e045c59a680d0450c9103a +#: ../../File_Transfer/File_Transfer.rst:867 4cf80f9f34e045c59a680d0450c9103a msgid "``file_transfer_client_recv_server_start``" msgstr "``file_transfer_client_recv_server_start``" -#: ../../File_Transfer/File_Transfer.rst:857 c09f5fcba03f461f89238dd31abf6e88 +#: ../../File_Transfer/File_Transfer.rst:868 c09f5fcba03f461f89238dd31abf6e88 msgid "``file_transfer_client_recv_server_start_thread``" msgstr "``file_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:858 ad39bc6b71f048b09bca3b895e9d32f8 +#: ../../File_Transfer/File_Transfer.rst:869 ad39bc6b71f048b09bca3b895e9d32f8 msgid "``file_folder_transfer_client_recv_server_start_thread``" msgstr "``file_folder_transfer_client_recv_server_start_thread``" -#: ../../File_Transfer/File_Transfer.rst:860 99140d0be72649199911a57a26e2f2cf +#: ../../File_Transfer/File_Transfer.rst:871 99140d0be72649199911a57a26e2f2cf msgid "(The low-level helpers are documented but not part of the public API.)" msgstr "(低階幫助程序已記錄,但不屬於公共 API 的一部分。)" -#: ../../File_Transfer/File_Transfer.rst:864 60355543ac904504af8529431ce2c1fa +#: ../../File_Transfer/File_Transfer.rst:875 60355543ac904504af8529431ce2c1fa msgid "See Also" msgstr "參見" -#: ../../File_Transfer/File_Transfer.rst:866 7a5902ad3de64bf79d54d7f2f83ecbfb +#: ../../File_Transfer/File_Transfer.rst:877 7a5902ad3de64bf79d54d7f2f83ecbfb msgid "" "For more information about the TCP server and client base classes, please " "refer to:" msgstr "關於TCP伺服器和客戶端基類的更多信息,請參考:" -#: ../../File_Transfer/File_Transfer.rst:870 f10c29f467b849e8b4254998b44f99ba +#: ../../File_Transfer/File_Transfer.rst:881 f10c29f467b849e8b4254998b44f99ba msgid ":doc:`../Network_APIs/TCP_Server_APIs`" msgstr ":doc:`../Network_APIs/TCP_Server_APIs`" -#: ../../File_Transfer/File_Transfer.rst:871 335ba244d28342449db065c252d7e14c +#: ../../File_Transfer/File_Transfer.rst:882 335ba244d28342449db065c252d7e14c msgid ":doc:`../Network_APIs/TCP_Client_APIs`" msgstr ":doc:`../Network_APIs/TCP_Client_APIs`" -#: ../../File_Transfer/File_Transfer.rst:873 92d0227c356447a098cba072d5b43c98 +#: ../../File_Transfer/File_Transfer.rst:884 92d0227c356447a098cba072d5b43c98 msgid "" "For details on port allocation, see the Port Allocation API sections in " "those documents." diff --git a/docs/locale/zh_TW/LC_MESSAGES/Instance_Setup/Instance_Setup.po b/docs/locale/zh_TW/LC_MESSAGES/Instance_Setup/Instance_Setup.po index 676c0fb..80ac664 100644 --- a/docs/locale/zh_TW/LC_MESSAGES/Instance_Setup/Instance_Setup.po +++ b/docs/locale/zh_TW/LC_MESSAGES/Instance_Setup/Instance_Setup.po @@ -8,20 +8,20 @@ msgid "" msgstr "" "Project-Id-Version: PyFlow\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-30 23:45+0800\n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.18.0\n" -#: ../../Instance_Setup/Instance_Setup.rst:3 707444a8cdb246fd81a189c038518c82 +#: ../../Instance_Setup/Instance_Setup.rst:3 552b0fbec3774a638b0029ce5fd72949 msgid "Flow Setup Launcher" msgstr "流程設定啟動器" -#: ../../Instance_Setup/Instance_Setup.rst:5 a611d1bbc1bc4b12a9db0997909626bc +#: ../../Instance_Setup/Instance_Setup.rst:5 31952c3324264ea28368c7725796ff63 msgid "" "The ``flow_setup.py`` script is a launcher for the TCP server/client " "framework defined in ``connect_tcp.py``. It allows you to quickly spawn a " @@ -32,7 +32,7 @@ msgstr "" "“flow_setup.py” 腳本是“connect_tcp.py” 中定義的 TCP " "伺服器/客戶端框架的啟動器。它允許您以互動方式或透過命令列參數快速產生單一伺服器或客戶端實例。每個啟動的實例都在單獨的終端視窗(或無頭系統上的後台進程)中運作。" -#: ../../Instance_Setup/Instance_Setup.rst:12 bc0bbe4590e04437827678fe85f482fb +#: ../../Instance_Setup/Instance_Setup.rst:12 4f99dd7362fa4dcb8f83de37f327dbef msgid "" "**Note:** This launcher supports only **one server** and **one client** " "instance at a time. Adding a new server or client configuration will " @@ -40,20 +40,20 @@ msgid "" msgstr "" "**注意:** 此啟動器一次僅支援 **一個伺服器** 和 **一個客戶端** 實例。新增的伺服器或用戶端配置將完全覆蓋任何先前的相同類型的配置。" -#: ../../Instance_Setup/Instance_Setup.rst:18 146d87f809054469be52a0d4174907fc +#: ../../Instance_Setup/Instance_Setup.rst:18 c8f9652279de42bb983523a15048596e msgid "Features" msgstr "特徵" -#: ../../Instance_Setup/Instance_Setup.rst:20 ea5ca94177b349b3852287e9327f792e +#: ../../Instance_Setup/Instance_Setup.rst:20 fba5b9ea870c44fabf69022a45cdaebc msgid "" "**Interactive mode** – step‑by‑step creation of a server or client instance." msgstr "**互動模式** – 逐步建立伺服器或客戶端實例。" -#: ../../Instance_Setup/Instance_Setup.rst:22 bb06f53ddbea4dd09b99e288569f858d +#: ../../Instance_Setup/Instance_Setup.rst:22 df9cbf9e1d024f9386a4b6278c82249e msgid "**Command‑line mode** – launch with all parameters in one command." msgstr "**命令列模式** – 在一個命令中使用所有參數啟動。" -#: ../../Instance_Setup/Instance_Setup.rst:24 11bd4b3ce97743e0857c109ae9bc2286 +#: ../../Instance_Setup/Instance_Setup.rst:24 5f7cba6df26144e9840d140f94065b33 msgid "" "**Persistent configuration** – stores the latest instance definitions in " "``setup.json`` (same directory as the script). Each type (server/client) " @@ -62,14 +62,14 @@ msgstr "" "**持久性配置** – " "將最新的實例定義儲存在「setup.json」(與腳本相同的目錄)中。每種類型(伺服器/客戶端)僅保留一個配置,該配置在每次更新時都會被覆寫。" -#: ../../Instance_Setup/Instance_Setup.rst:29 7b3f201c93c349719f4128f05cc194bf +#: ../../Instance_Setup/Instance_Setup.rst:29 be9bc008af594306be21e731d6aefee3 msgid "" "**Cross‑platform** – supports Windows (cmd), Linux (gnome‑terminal, xterm, " "or background), and macOS (Terminal.app)." msgstr "" "**跨平台** – 支援 Windows (cmd)、Linux(gnome 終端機、xterm 或後台)和 macOS (Terminal.app)。" -#: ../../Instance_Setup/Instance_Setup.rst:32 89b17f1fa60241a288381f74072aa273 +#: ../../Instance_Setup/Instance_Setup.rst:32 57005cb0d04f47b2a8f10ad0bc9c7b43 msgid "" "**Complete parameter support** – all parameters accepted by " "``TCP_Server_Base`` and ``TCP_Client_Base`` can be stored in ``setup.json`` " @@ -78,107 +78,153 @@ msgstr "" "**完整的參數支援** – ``TCP_Server_Base`` 和 ``TCP_Client_Base`` " "接受的所有參數都可以儲存在``setup.json`` 中以進行微調。" -#: ../../Instance_Setup/Instance_Setup.rst:37 ca859ffa1d4140cab68092170525c59f +#: ../../Instance_Setup/Instance_Setup.rst:37 2ff27919a6f64e8fb610158de5ffdf69 msgid "Usage" msgstr "用法" -#: ../../Instance_Setup/Instance_Setup.rst:40 1935db82ef7f4dbeb08d386a49aba876 +#: ../../Instance_Setup/Instance_Setup.rst:40 dddde4f2d7084bc48efa1296cfd3f22c msgid "Interactive Mode" msgstr "互動模式" -#: ../../Instance_Setup/Instance_Setup.rst:42 86f8dafb1c9d4adaa4666b1dea52af23 +#: ../../Instance_Setup/Instance_Setup.rst:42 f060e64959674f52b223bbc4b01d568c msgid "Run the script without any arguments:" msgstr "不帶任何參數運行腳本:" -#: ../../Instance_Setup/Instance_Setup.rst:48 d516fe21071e448da15df91f07353fc8 +#: ../../Instance_Setup/Instance_Setup.rst:48 dd76918a5ffd44838c1b0c44595ae55b msgid "The script will ask you to:" msgstr "該腳本將要求您:" -#: ../../Instance_Setup/Instance_Setup.rst:50 41684f03f6d7460f87fed8f4bef8f1e5 +#: ../../Instance_Setup/Instance_Setup.rst:50 80847dcc3e404a779b2e693cf8de50d5 msgid "Choose the type (0 for Server, 1 for Client)." msgstr "選擇類型(0 表示伺服器,1 表示客戶端)。" -#: ../../Instance_Setup/Instance_Setup.rst:51 750440862d7545e9bc4f137f36983fd6 +#: ../../Instance_Setup/Instance_Setup.rst:51 cebc8ba62d1a48a69d8019c3862dc18b msgid "Enter the bind address and port (``host:port``)." msgstr "輸入綁定位址和連接埠(``host:port``)。" -#: ../../Instance_Setup/Instance_Setup.rst:52 a938572ed1dd43f7988e382ec9fea306 +#: ../../Instance_Setup/Instance_Setup.rst:52 4311b66e1b974b4596f2693908ba3109 msgid "If Client, also enter the server address and port to connect to." msgstr "如果是客戶端,還需輸入要連接的伺服器位址和連接埠。" -#: ../../Instance_Setup/Instance_Setup.rst:53 3f30b7f3c46e42c19eae6344b424e1f7 +#: ../../Instance_Setup/Instance_Setup.rst:53 65c194c1b655417c921c1e03daf7a17c msgid "" "Decide whether to add another instance (if you add the same type again, the " "previous configuration of that type is overwritten)." msgstr "決定是否要新增其他實例(如果再次新增相同類型,則覆寫該類型先前的配置)。" -#: ../../Instance_Setup/Instance_Setup.rst:55 e8b6550fe5eb426f8478c1b3fef81160 +#: ../../Instance_Setup/Instance_Setup.rst:55 6bb60d44328244d4ab20f3ae5ba2b827 msgid "" "If ``setup.json`` already exists, you will be prompted to either reuse the " "existing configuration (launch the stored instances) or overwrite it with " "new definitions." msgstr "如果「setup.json」已存在,系統將提示您重複使用現有配置(啟動儲存的實例)或使用新定義覆寫它。" -#: ../../Instance_Setup/Instance_Setup.rst:60 b222bf87298747c6b526d72e1459ad83 +#: ../../Instance_Setup/Instance_Setup.rst:60 9aeb2efa913341a89212e86cb7419e5a msgid "" "**Important:** When you choose to overwrite, the old server/client " "configuration is **completely replaced** by the new one. There is no " "merging." msgstr "**重要:** 當您選擇覆蓋時,舊的伺服器/客戶端設定將被新的設定**完全取代**。沒有合併。" -#: ../../Instance_Setup/Instance_Setup.rst:65 ad560cd1d404495583c74b5929235453 +#: ../../Instance_Setup/Instance_Setup.rst:65 b5cd8be69a40420a89cf7ad69631cd45 msgid "Command‑line Mode" msgstr "命令列模式" -#: ../../Instance_Setup/Instance_Setup.rst:67 50a9125f52a54c9b82c66cf616e06d71 +#: ../../Instance_Setup/Instance_Setup.rst:67 d798ec606de2454da5f10e9d0a7e677c msgid "Use the following options:" msgstr "使用以下選項:" -#: ../../Instance_Setup/Instance_Setup.rst:82 f7f3b3263fdf457c908ea18b74dccea9 +#: ../../Instance_Setup/Instance_Setup.rst:70 4d5c1aebbede473c8598cbc45f8deb20 +msgid "Option" +msgstr "選項" + +#: ../../Instance_Setup/Instance_Setup.rst:70 a50ba126b168482997fae00497252fd4 +msgid "Description" +msgstr "描述" + +#: ../../Instance_Setup/Instance_Setup.rst:72 7755904907ee46f6a34144360ed876f7 +#, python-brace-format +msgid "``--type {0,1}``" +msgstr "`` --type {0,1} ``" + +#: ../../Instance_Setup/Instance_Setup.rst:72 e0533cce8d9f4586aa838073b1ed3e2a +msgid "**Required.** 0 = Server, 1 = Client." +msgstr "* *必需。* * 0 =伺服器, 1 =用戶端。" + +#: ../../Instance_Setup/Instance_Setup.rst:74 7cc185b424ee489282d9d85571d54fae +msgid "``--setup_addr_port``" +msgstr "`` --setup_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:74 37df8730dce9408a87ba48b775eb2b0a +#, fuzzy +msgid "**Required.** Bind address and port (e.g. ``127.0.0.1:8000``)." +msgstr "輸入綁定位址和連接埠(``host:port``)。" + +#: ../../Instance_Setup/Instance_Setup.rst:77 388a3dd952ac47a78598792ef7d587bc +msgid "``--connect_addr_port``" +msgstr "`` --connect_addr_port ``" + +#: ../../Instance_Setup/Instance_Setup.rst:77 b4f319ec304e4a26a09ac62b90db0a07 +#, fuzzy +msgid "Required for Client only. Server address and port to connect to." +msgstr "如果是客戶端,還需輸入要連接的伺服器位址和連接埠。" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7bb925846a36488d945c3442889d83c0 +msgid "``--setup_num``" +msgstr "`` --setup_num ``" + +#: ../../Instance_Setup/Instance_Setup.rst:80 7f3e0b1925924583acf7acb25ec6e183 +msgid "" +"*Ignored.* The script always launches a single instance. This flag is " +"accepted for compatibility but has no effect." +msgstr "*忽略。*腳本總是啟動單個實例。為了相容性,此旗標已被接受,但無效。" + +#: ../../Instance_Setup/Instance_Setup.rst:86 99952fa68b694654b82a309a26419152 msgid "Examples" msgstr "範例" -#: ../../Instance_Setup/Instance_Setup.rst:84 13847166f8d54c9db943e1e55cf66c55 +#: ../../Instance_Setup/Instance_Setup.rst:88 9c77e76f40a940e99e754d27e3bc1b05 msgid "**Launch a single server** on ``127.0.0.1:8000``:" msgstr "**在「127.0.0.1:8000」上啟動單一伺服器**:" -#: ../../Instance_Setup/Instance_Setup.rst:90 aa92290b83324661a23d2343f7ca56fb +#: ../../Instance_Setup/Instance_Setup.rst:94 37df8730dce9408a87ba48b775eb2b0a msgid "" "**Launch a client** bound to port ``9000``, connecting to a server at " "``127.0.0.1:8000``:" msgstr "**啟動綁定到連接埠「9000」的客戶端**,連接到「127.0.0.1:8000」的伺服器:" -#: ../../Instance_Setup/Instance_Setup.rst:97 9d2ca3944f984434b4ecb579b075ccc4 +#: ../../Instance_Setup/Instance_Setup.rst:101 +#: 0bdae3cd584e464ab526840aa032e3a4 msgid "" "**Launch from an existing configuration** (if ``setup.json`` is present):" msgstr "**從現有配置啟動**(如果存在“setup.json”):" -#: ../../Instance_Setup/Instance_Setup.rst:105 -#: d88aa64a570f4f36baf5c7c92d4bd861 +#: ../../Instance_Setup/Instance_Setup.rst:109 +#: ff45af42eb7d4d3faa8640a18bfd61f6 msgid "Configuration File" msgstr "設定檔" -#: ../../Instance_Setup/Instance_Setup.rst:107 -#: cfb0db3d5e23418d9c93ddb2d34548d4 +#: ../../Instance_Setup/Instance_Setup.rst:111 +#: 39e34a4760354d78b87a8e050029e197 msgid "" "The script writes a file named ``setup.json`` in the same directory. Its " "structure is:" msgstr "該腳本在同一目錄中寫入一個名為「setup.json」的檔案。其結構為:" -#: ../../Instance_Setup/Instance_Setup.rst:131 -#: 0a2b6c0e312a49de8ed7838e045b3b66 +#: ../../Instance_Setup/Instance_Setup.rst:135 +#: 9e72e7ecd43c498299e24f5598b6f2b8 msgid "" "**Each list contains at most one object.** When a new server or client " "configuration is added, the entire list for that type is replaced." msgstr "**每個清單最多包含一個物件。 ** 新增新的伺服器或用戶端配置時,該類型的整個清單都會被取代。" -#: ../../Instance_Setup/Instance_Setup.rst:136 -#: 1a256391599446d99c3cc375639f251c +#: ../../Instance_Setup/Instance_Setup.rst:140 +#: 8731a083a201487bb579beb4a238a0db msgid "Custom Parameters" msgstr "自訂參數" -#: ../../Instance_Setup/Instance_Setup.rst:138 -#: 36cefa4820cd42ee93fdb560be1b1032 +#: ../../Instance_Setup/Instance_Setup.rst:142 +#: 1568543fd757484da86f12a72ccb58b5 msgid "" "You can manually edit ``setup.json`` to include any parameter accepted by " "``TCP_Server_Base`` or ``TCP_Client_Base`` (see the source code for the full" @@ -192,68 +238,84 @@ msgstr "" "您可以手動編輯“setup.json”以包含“TCP_Server_Base”或“TCP_Client_Base”接受的任何參數(完整清單請參閱原始程式碼)。當啟動器覆蓋配置時,這些自訂值將被保留(因為腳本讀取現有配置並使用使用者提供的值更新它,但如果您選擇覆蓋,舊配置將被丟棄,僅保存新欄位" " - 因此,如果您需要自訂參數,您應該在首次啟動後新增它們或手動編輯檔案)。" -#: ../../Instance_Setup/Instance_Setup.rst:150 -#: 6b90a320b39147c7ac5287c51b028da3 +#: ../../Instance_Setup/Instance_Setup.rst:154 +#: c62b1bfe8ceb40f8865a34ac87b4ba18 msgid "Extension Protocols and Startup Mode" msgstr "擴展協定和啟動模式" -#: ../../Instance_Setup/Instance_Setup.rst:152 -#: 81cf1090db0240e19aecfb16cdafb622 +#: ../../Instance_Setup/Instance_Setup.rst:156 +#: e6a7dcb4ea184e828e6667ae6505d872 msgid "" "Two extension protocols ship with the launcher and are loaded automatically " "for every instance whose ``setup.json`` entry sets " "``is_extend_command=True``:" msgstr "啟動器隨附兩個擴充協議,並為每個「setup.json」條目設定「is_extend_command=True」的實例自動載入:" -#: ../../Instance_Setup/Instance_Setup.rst:156 -#: e89028b886064e8ab1e94e5e6a6cdaa0 -msgid "``command_control_extension_tcp.py`` – remote command" +#: ../../Instance_Setup/Instance_Setup.rst:160 +#: 7159f768fc9a4ac199bfe0a4d8478fba +#, fuzzy +msgid "" +"``command_control_extension_tcp.py`` – remote command execution with per-" +"client log collection (``/command``)." msgstr "``command_control_extension_tcp.py`` – 遠端命令" -#: ../../Instance_Setup/Instance_Setup.rst:157 -#: 86a457e4e5bf4791bcbef5bb12612391 +#: ../../Instance_Setup/Instance_Setup.rst:162 +#: d2d277053c74470abc949b891493a5f0 +#, fuzzy msgid "" -"execution with per-client log collection (``/command``). - " -"``forward_extension_tcp.py`` – forwarding messages, files, multiple files, " -"folders and multiple folders to any number of destination clients " -"(``/send_msg_forward``, ``/file_forward``, ``/multiple_file_forward``, " -"``/folder_forward``, ``/multiple_folder_forward``)." +"``forward_extension_tcp.py`` – forwarding files, multiple files, folders and" +" multiple folders to any number of destination clients (``/file_forward``, " +"``/multiple_file_forward``, ``/folder_forward``, " +"``/multiple_folder_forward``)." msgstr "" "使用每個客戶端日誌收集執行(``/command``)。 -``forward_extension_tcp.py`` - " "将消息、文件、多个文件、文件夹和多个文件夹转发到任意数量的目标客户端(``/send_msg_forward``、``/file_forward``、``/multiple_file_forward``、``/folder_forward``、``/multiple_folder_forward``)。" -#: ../../Instance_Setup/Instance_Setup.rst:164 -#: baee7200e5634507a28b2f69309c3c49 +#: ../../Instance_Setup/Instance_Setup.rst:168 +#: a8194174e5fd4dcf87ede716eaada9a4 +msgid "" +"Plain-message forwarding is native to the TCP protocol (no extension " +"needed): the client-only command ``/forward_send_msg`` relays messages to " +"the listed destination clients through the server." +msgstr "" +"純訊息轉發原生於TCP通訊協定(無需擴充) :僅用戶端指令``/forward_send_msg ``透過伺服器將訊息轉送至列出的目標用戶端。" + +#: ../../Instance_Setup/Instance_Setup.rst:173 +#: 98297b76f3404736b92b6c507726a50a msgid "" "With ``is_extend_command=False`` (the default) only the raw TCP protocol is " "started." msgstr "使用“is_extend_command=False”(預設)僅啟動原始 TCP 協定。" -#: ../../Instance_Setup/Instance_Setup.rst:167 -#: f140a881acba4933bd83af5bb35737d1 +#: ../../Instance_Setup/Instance_Setup.rst:176 +#: 9b5ab0319a9843f1b39721d3a96d1777 msgid "" "The ``is_input_command_in_console`` flag selects how the instance is " "started:" msgstr "``is_input_command_in_console`` 標誌選擇實例的啟動方式:" -#: ../../Instance_Setup/Instance_Setup.rst:170 -#: a40aa238baac4503a0dc0c9cea0745ee -msgid "``True`` (default) – ``start_TCP_Server()`` /" -msgstr "``True``(預設)-``start_TCP_Server()`` /" +#: ../../Instance_Setup/Instance_Setup.rst:179 +#: 1ec730c995b1454da1b208331d9ca8fe +msgid "" +"``True`` (default) – ``start_TCP_Server()`` / ``start_TCP_client()`` is " +"called directly and the console input loop runs in its own thread." +msgstr "" +"`` True `` (預設) – `` START_TCP_SERVER () ``/`` START_TCP_CLIENT () " +"``被直接呼叫,主控臺輸入迴圈在其自己的執行緒中執行。" -#: ../../Instance_Setup/Instance_Setup.rst:171 -#: fca5c2c0fe914740a931957b30a9927b +#: ../../Instance_Setup/Instance_Setup.rst:182 +#: 3cfb2eba312943cea85388f4e9faf40d +#, fuzzy msgid "" -"``start_TCP_client()`` is called directly and the console input loop runs in" -" its own thread. - ``False`` – the instance runs in a background thread and " -"the launcher keeps the process alive until the instance stops (useful for " -"headless deployments)." +"``False`` – the instance runs in a background thread and the launcher keeps " +"the process alive until the instance stops (useful for headless " +"deployments)." msgstr "" "直接呼叫“start_TCP_client()”,控制台輸入循環在其自己的執行緒中運行。 -``False`` - " "實例在後台執行緒中執行,啟動器可讓進程保持活動狀態,直到實例停止(對於無頭部署很有用)。" -#: ../../Instance_Setup/Instance_Setup.rst:177 -#: c2925f0f85154040bcdd1252a228fd7f +#: ../../Instance_Setup/Instance_Setup.rst:186 +#: 3b3482fa903f4afd81234e588a229c1f msgid "" "Both extensions also expose injectable registration " "(``setup_server_commands(instance)`` / ``setup_client_commands(instance)``) " @@ -267,62 +329,67 @@ msgstr "" "is_input_command_in_console=True)`` /``server_`` /``server_ " "is_input_command_in_console=True)`` 接受現有實例,因此可以載入多個擴充功能從程式碼到同一個實例。" -#: ../../Instance_Setup/Instance_Setup.rst:186 -#: e58290a2c5f3492bafa47f15b7c2958f +#: ../../Instance_Setup/Instance_Setup.rst:195 +#: 1dc7b46c0e3047d2885573cc4f55e7c6 msgid "Internal Operation" msgstr "內部運作" -#: ../../Instance_Setup/Instance_Setup.rst:188 -#: 6655764b01644b92910338c01d38b0cb -msgid "Each instance is launched in a new terminal window" +#: ../../Instance_Setup/Instance_Setup.rst:197 +#: 8870bc6b731d40a095a40faed0c8f16d +#, fuzzy +msgid "" +"Each instance is launched in a new terminal window (or background process)." msgstr "每個實例都在新的終端機視窗中啟動" -#: ../../Instance_Setup/Instance_Setup.rst:189 -#: 6824028a99254c77a55fb56d51b5e9a5 -msgid "(or background process)." -msgstr "(或後台進程)。" - -#: ../../Instance_Setup/Instance_Setup.rst:190 -#: 0a09353648d44ba2a840fc1cf8a850bc -msgid "The configuration is passed via a temporary JSON" +#: ../../Instance_Setup/Instance_Setup.rst:199 +#: 13c720a93ba54e8ca330fce3a078bb5b +#, fuzzy +msgid "" +"The configuration is passed via a temporary JSON file to avoid shell " +"escaping issues." msgstr "配置透過臨時 JSON 傳遞" -#: ../../Instance_Setup/Instance_Setup.rst:191 -#: f6327281e35a4304b404563d00301a1a -msgid "file to avoid shell escaping issues." -msgstr "文件以避免 shell 轉義問題。" - -#: ../../Instance_Setup/Instance_Setup.rst:192 -#: 997e364f35714b18b1e47af641ce58db -msgid "If an instance fails to start, the error is" -msgstr "如果實例無法啟動,則錯誤為" - -#: ../../Instance_Setup/Instance_Setup.rst:193 -#: 4404c088296e4c35b49ed59eca3b6678 -msgid "displayed and the window pauses for inspection." +#: ../../Instance_Setup/Instance_Setup.rst:201 +#: 9566eb3cb70e4b77bcbdb584f7e0b6fc +#, fuzzy +msgid "" +"If an instance fails to start, the error is displayed and the window pauses " +"for inspection." msgstr "顯示並且視窗暫停以進行檢查。" -#: ../../Instance_Setup/Instance_Setup.rst:196 -#: 2b8f982006184d36b06d4ac5579a36f4 +#: ../../Instance_Setup/Instance_Setup.rst:205 +#: 5f0f0f6a41384356986a1182f14a0a4c msgid "Requirements" msgstr "要求" -#: ../../Instance_Setup/Instance_Setup.rst:198 -#: f10fabd9883942b780cd0822da245837 +#: ../../Instance_Setup/Instance_Setup.rst:207 +#: 49ceb74e41104b00971cd9edecef08ae msgid "Python 3.6+" msgstr "Python 3.6+" -#: ../../Instance_Setup/Instance_Setup.rst:199 -#: f464e1a571fa46388937f3236b36d3bf +#: ../../Instance_Setup/Instance_Setup.rst:208 +#: 541cd1be44c044f3880eb220a87c06b7 msgid "The ``network_api.connect_tcp`` module must be" msgstr "``network_api.connect_tcp`` 模組必須是" -#: ../../Instance_Setup/Instance_Setup.rst:200 -#: fc6ca227cd904333916e81b4a7093acc +#: ../../Instance_Setup/Instance_Setup.rst:209 +#: 427fef8d7c5047fc934681026969639a msgid "importable (the script imports ``TCP_Server_Base``" msgstr "可匯入(腳本匯入``TCP_Server_Base``" -#: ../../Instance_Setup/Instance_Setup.rst:201 -#: dcc9c410853247b0a3302c2c41f6fe13 +#: ../../Instance_Setup/Instance_Setup.rst:210 +#: fb35af261d664664bdb3c1b3aaac3e83 msgid "and ``TCP_Client_Base`` from there)." msgstr "和來自那裡的“TCP_Client_Base”)。" + +#~ msgid "``True`` (default) – ``start_TCP_Server()`` /" +#~ msgstr "``True``(預設)-``start_TCP_Server()`` /" + +#~ msgid "(or background process)." +#~ msgstr "(或後台進程)。" + +#~ msgid "file to avoid shell escaping issues." +#~ msgstr "文件以避免 shell 轉義問題。" + +#~ msgid "If an instance fails to start, the error is" +#~ msgstr "如果實例無法啟動,則錯誤為" diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.add_extension.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.add_extension.po new file mode 100644 index 0000000..55c69a1 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.add_extension.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.add_extension.rst:2 239fa5edd0d944bd884b21d803e0f4a1 +msgid "PyFlow.add\\_extension module" +msgstr "" + +#: PyFlow.add_extension.copy_extension_files:1 c4a96a18a6754f1fb4c5c3a9ff9dd518 +#: of +msgid "Validate extension path(s) and return them as a list." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst 4b8e1285572947c4a34dbcd9e22fb52c +#: 78eb00a995d84b028e95127f753b4fb5 PyFlow.add_extension.remove_extension +#: dff4aabd6aad43e188c9fc9b73d9142b of +msgid "Parameters" +msgstr "" + +#: 49c2e6f1496d433ab7a9d162802419f1 5de3fc7a955443cb9bef23851780925f +#: PyFlow.add_extension.add_extension:3 +#: PyFlow.add_extension.copy_extension_files:3 +#: PyFlow.add_extension.remove_extension:3 b82537ecca1e4f418a5a6aacacdd900c of +msgid "a single path string or a list of path strings." +msgstr "" + +#: ../../api/PyFlow.add_extension.rst fa91fb96a98f4374b2da5085d1373ef4 +msgid "Returns" +msgstr "" + +#: 450992bbbe91432fb44b0265e58d8f59 PyFlow.add_extension.copy_extension_files:5 +#: of +msgid "The original paths as a list (extensions are not copied)." +msgstr "" + +#: 487ed0fee55540c796c92b88d0a8b2ea +#: PyFlow.add_extension.add_added_extension_logs:1 of +msgid "Append paths to the extension registration log file." +msgstr "" + +#: PyFlow.add_extension.add_extension:1 cd718206d465487ebd43c17e796c4da5 of +msgid "Register extension file(s) in added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.remove_extension:1 bb4313796aff49a1b96a37d66912f18a of +msgid "Remove registered extension path(s) from added_extensions.json." +msgstr "" + +#: 6520ce551e5f40e6afe59c549d15e493 PyFlow.add_extension.remove_extension:5 of +msgid "If the registration file does not exist, this is a no-op." +msgstr "" + +#: 26339cf4e8a041f798d969a890592971 +#: PyFlow.add_extension.load_registered_extensions:1 of +msgid "Load every registered extension from added_extensions.json." +msgstr "" + +#: PyFlow.add_extension.load_registered_extensions:3 +#: e53cceb4fe384c1fa10ddf1905998823 of +msgid "" +"For each registered path, the module is imported dynamically and its " +"``setup_server_commands(instance)`` or " +"``setup_client_commands(instance)`` is called, depending on " +"*instance_type*." +msgstr "" + +#: 11efbd89b0254bb190c21005396f53dd +#: PyFlow.add_extension.load_registered_extensions:7 of +msgid "" +"Raises ImportError if the JSON file is reachable but a module cannot be " +"imported or loaded, or if the required setup function is missing." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po new file mode 100644 index 0000000..dac09ff --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.command_control_extension_tcp.po @@ -0,0 +1,36 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.command_control_extension_tcp.rst:2 +#: a6084f2941f34f579f28c55e9dfb768d +msgid "PyFlow.command\\_control\\_extension\\_tcp module" +msgstr "" + +#: 9209095513ec402980427a0ddd988219 +#: PyFlow.command_control_extension_tcp.setup_server_commands:1 of +msgid "Register the control-extension commands on a server instance." +msgstr "" + +#: 114b0a54c29747fd88bc2852eab174bf +#: PyFlow.command_control_extension_tcp.setup_client_commands:1 of +msgid "Register the control-extension commands on a client instance." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.flow_setup.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.flow_setup.po new file mode 100644 index 0000000..6da817b --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.flow_setup.po @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.flow_setup.rst:2 a6a54598744740409fe0b28f73834ffe +msgid "PyFlow.flow\\_setup module" +msgstr "" + +#: 586460f36f3b4654abb0db6b5d77b39d PyFlow.flow_setup.launch_web_tool:1 of +msgid "Launch the transfer_web launcher (``kind`` = \"server\" or \"client\")." +msgstr "" + +#: PyFlow.flow_setup.launch_web_tool:3 ef1274ba70124aeeadee3d4cfcca99c2 of +msgid "" +"The web tool is a Flask app that opens a browser UI, so it runs in its " +"own process (a terminal window when one is available, otherwise detached)" +" and the launcher returns immediately." +msgstr "" + +#: 7086887643164f229f919546dceb0e36 PyFlow.flow_setup.edit_existing_instances:1 +#: of +msgid "Vim-style editor to delete/change existing instances." +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:3 d04ffe1cf853403292908fc69962f7dc +#: of +msgid "Returns (status, servers, clients):" +msgstr "" + +#: 5a751731515c4adfba9787fdd5e93215 PyFlow.flow_setup.edit_existing_instances:4 +#: of +msgid "status == \"saved\" -> setup.json was written (:w / :wq); keep the" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:5 e61a6437dae14d1e9f9ac4e6848cd444 +#: of +msgid "returned edited lists" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:6 bb9424a8ea894dbb888df4fa2e0875ed +#: of +msgid "status == \"discarded\" -> the editor was exited without saving" +msgstr "" + +#: PyFlow.flow_setup.edit_existing_instances:7 d0d81a426f754475922c3e9f4aee6627 +#: of +msgid "(:q! / :q) and the original lists are returned unchanged" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po new file mode 100644 index 0000000..96fe16a --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.forward_extension_tcp.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.forward_extension_tcp.rst:2 +#: 39b0ca283c7c48b08f66a67eb80766b0 +msgid "PyFlow.forward\\_extension\\_tcp module" +msgstr "" + +#: PyFlow.forward_extension_tcp:1 ad6b75f791c543dea48894cd8bac6941 of +msgid "Forward extension for the TCP protocol." +msgstr "" + +#: PyFlow.forward_extension_tcp:3 ff4264b5fba24d7d93e9bb3e398edfd5 of +msgid "" +"Disk-based, upload-then-push forwarding of files and folders to a list of" +" destination clients. This is deliberately a second implementation of " +"file forwarding: the native TCP protocol already streams files and " +"folders in memory (``/forward_file`` / ``/forward_folder`` on a client " +"console, relayed by the server as ``/forward_item`` with no disk I/O on " +"the server), while this extension uploads the data to the server's " +"transfer directory first and then asks the server to push the stored " +"copies. Plain-message forwarding is native as well (the client-only " +"command ``/forward_send_msg``, relayed by the server), so no string " +"forwarding lives here." +msgstr "" + +#: 2f49002886224f5bb68eb09f2c4a8a30 PyFlow.forward_extension_tcp:14 of +msgid "Transfer families added by this extension:" +msgstr "" + +#: PyFlow.forward_extension_tcp:16 ab1c45d0c28345c2898a54c66c98416b of +msgid "/file_forward <(ip, port)> ..." +msgstr "" + +#: 93ef2915b1fa4bd9b4b42e8e6ec747f8 PyFlow.forward_extension_tcp:17 of +msgid "forward one file to every listed destination" +msgstr "" + +#: 9773fb5cd9414f0aabb496ae3a32005a PyFlow.forward_extension_tcp:18 of +msgid "/multiple_file_forward ... <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:19 ee54881259274d6b9085bf71b44d659a of +msgid "forward several files to every listed destination" +msgstr "" + +#: 19144093ca8d45e988dfcfe09221bf6a PyFlow.forward_extension_tcp:20 of +msgid "/folder_forward <(ip, port)> ..." +msgstr "" + +#: PyFlow.forward_extension_tcp:21 b73d9263c9874b1e9ea5a714138d72d8 of +msgid "forward one folder (structure preserved) to every destination" +msgstr "" + +#: 4f7e8bb4b31c4eddbb78d815e85f8795 PyFlow.forward_extension_tcp:22 of +msgid "/multiple_folder_forward ... <(ip, port)> ..." +msgstr "" + +#: 9e95e860d39744f8ba3d52ddaf357943 PyFlow.forward_extension_tcp:23 of +msgid "forward several folders to every listed destination" +msgstr "" + +#: 612162f14a574132bf396a1b6ea853f1 PyFlow.forward_extension_tcp:25 of +msgid "" +"Items come first, destinations last; every destination is written as a " +"Python address tuple, e.g. ``\"('127.0.0.1', 3000)\"``. There is no limit" +" on the number or size of items or destinations." +msgstr "" + +#: 1b0ec7328493432fb876117b1f76bb3b PyFlow.forward_extension_tcp:29 of +msgid "" +"The commands are only available on the client console: they are " +"registered in the \"client\" handler group, so typing them on the server " +"console is rejected as an unrecognized command. Forwarding goes through " +"the server - the client uploads the data over the normal transfer channel" +" (the server stores it in its transfer directory) and then asks the " +"server to push it to the destinations, which receive it through the main " +"protocol's own receive paths. Destinations that are unreachable (not " +"connected to the server, or the server itself, which is never in the " +"client table) are skipped and the remaining destinations are still " +"served." +msgstr "" + +#: 443d9fe7a21148d984a0e3f2f3cb2c28 +#: PyFlow.forward_extension_tcp.setup_client_commands:1 of +msgid "Register the file/folder forward commands on a client instance." +msgstr "" + +#: 5dbc099afeb54fbb859bf5760331adfd +#: PyFlow.forward_extension_tcp.setup_client_commands:3 of +msgid "" +"Message forwarding (``/forward_send_msg``) is native and needs no setup. " +"Each command binds its transfer kind and single/multiple policy into the " +"shared handler via functools.partial; where_to_run=\"client\" makes them " +"fire from console input only." +msgstr "" + +#: 392556d0850f44b1b64dae0fe65a748c +#: PyFlow.forward_extension_tcp.setup_server_commands:1 of +msgid "Register the file/folder forward relays on a server instance." +msgstr "" + +#: 399725c6728441568702225abc50b2c4 +#: PyFlow.forward_extension_tcp.setup_server_commands:3 of +msgid "" +"The message relay (``/forward_send_msg``) is native and needs no setup. " +"These handlers are triggered by relay requests sent by clients, i.e. they" +" live in the \"server\" group: messages coming in from other instances " +"are dispatched there. The /xxx_forward commands themselves stay in the " +"client group, so typing them on the server console is rejected as " +"unrecognized." +msgstr "" + +#: 337c29db84fe4d3099a2e3d5e135d0d1 PyFlow.forward_extension_tcp.client_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable client (mirrors the control " +"extension)." +msgstr "" + +#: 99ee11955438459d84f5c1ae6f3fedbb PyFlow.forward_extension_tcp.server_setup:1 +#: of +msgid "" +"Create and start a forwarding-capable server (mirrors the control " +"extension)." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po new file mode 100644 index 0000000..0a6ef87 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_tcp.po @@ -0,0 +1,1759 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_tcp.rst:2 +#: 1e4cd09ce3ef45ceb95b8eb81eb7493b +msgid "PyFlow.network\\_api.connect\\_tcp module" +msgstr "" + +#: 25b88cdb337c44539a843c674ba6825b PyFlow.network_api.connect_tcp:1 of +msgid "" +"TCP transport for PyFlow: the server and client base classes and the wire" +" parsers." +msgstr "" + +#: 1f1482fc5c2849a68f34c74a8b55ab79 PyFlow.network_api.connect_tcp:3 of +msgid "" +"``TCP_Server_Base`` accepts connections and dispatches inbound lines; " +"``TCP_Client_Base`` connects, sends and reads on the same conventions:" +msgstr "" + +#: 76e93df1b95547ff8bded710dc31340d PyFlow.network_api.connect_tcp:6 of +msgid "" +"one message per line, terminated by a newline; a line that starts with " +"``/`` is a command and goes to the command handlers, anything else is a " +"plain message reported to the registered message listeners;" +msgstr "" + +#: 2497ecf21ac947c78502a3839a452cb8 PyFlow.network_api.connect_tcp:9 of +msgid "" +"an RSA-encrypted channel is negotiated right after connect unless " +"``is_enable_encrypto`` is False;" +msgstr "" + +#: 9702905f1f8a4750905b2ffb96f99ec8 PyFlow.network_api.connect_tcp:11 of +msgid "" +"file/folder transfer, message forwarding and port allocation are layered " +"on the same socket and share its command namespace." +msgstr "" + +#: 4cc5743bdfe54148ad38f54f10e688c9 PyFlow.network_api.connect_tcp:14 of +msgid "" +"The forwarding extensions use the module-level parsers " +"`parse_forwarded_message`, `parse_forward_items_and_addrs`, " +"`parse_forward_originator` and `forward_skip_message`." +msgstr "" + +#: 49796529cb7f49ee8131b257212b5420 PyFlow.network_api.connect_tcp:18 of +msgid "" +"Concepts live in ``docs/Network_APIs/TCP_Server_APIs.rst`` and " +"``TCP_Client_APIs.rst``; argument, return and exception contracts live in" +" the docstrings below." +msgstr "" + +#: 39274682c4bd46489ed6fe535b50ede5 +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:1 of +msgid "Split a ``/send_msg_from `` relay envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07da14a120314eb7a3acd1c46ddfeaca 0b005c95146946208a01640628f50a57 +#: 10904afb15ee45b29e91a2ab5da0db26 14d9353e52a54c019428fb94211f17f6 +#: 1861e8bcd5bb41489f13265860311349 1c6ba1e0968a409499b7a262c34dcc2a +#: 20b95160870648538fd617ca4ce3d2b5 2351fc654dfc452a8d07e5d566265d9f +#: 27611fb8240a422db411c043f60bdbe9 464f4d62231e4a61b1a939ad2054b13c +#: 48bd66821c14451d9a0c682d9468a5a5 4e1c4be3e22f403396d5ee1788d0e3b0 +#: 4fa9ba6e763440b8ab00832520b4d305 501a0caef71d436e84469bc1ef9c1e5f +#: 52bdf8d502cc4a31a6df5ae543ceb182 531f1e6ff68e44bcac61633e2d7c511f +#: 5404b8d0f66848c3b1db9f08a25f92c0 55fc240d149746e190ecf924a2ca6dd4 +#: 59ab6a93eceb40f7b590133c3d2b8548 6d8d8704b9c94105928dbbdf15ce9f12 +#: 70d6dae11a144ed7b7e9d8e16dbf478c 71de1bb0c4d24062a819a6fe59013d34 +#: 74c64b0da3294322b512f3155955fb7a 76f08e34cd4a42c880880cfb511fcc66 +#: 7a6a9f1497ac48f5867783d78fdae37c 7c3501da0be44d78a671e96ba4384489 +#: 7e9c7fe1ae974c1082550fb3e6e3de1f 7fc19de66098437faef7bffed3b5f752 +#: 7fdf476d99ff4f54ad35cf4bb506e47d 812d9d039fe34f71aa4b662c4511c8e9 +#: 81f8f7d346ff45639f39eb0f033103df 95d8b328540c4094a4fcc8fdc9139645 +#: a163536aaa1f49399502da52fb481666 acdf8b3b266f4eccb6ceca17a110603b +#: b097aecf6ae74e17b36fc9e806d5b26a b3db16a6c2374980ae9b071d9f3f15e6 +#: c2a18448d83f4879bcb29f51ca31bc5b c5ed2382d01042c68a5e372a4c7de2ac +#: d36c8c10afba4732969367886b8663ab d6d746e16dbd47308eea5aacd9614f15 +#: d8e94668c5d7434fa61c2ffcbe73b6af dad1324e962d49419245fe3b88c20121 +#: dd3d356c14cb462e98ea42e3502dca80 eb24a57758194ab2bdef9e95362584df +#: ef5da8d6c91c469fad0871d213999d2a ff3615b7096e46bba8797b739bb954b2 +msgid "Parameters" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:3 +#: e42a6166c82d4861b25cb55f587db28c of +msgid "Received line, e.g. ``/send_msg_from ('127.0.0.1', 3000) hello``." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 07f0651994f343d7ac8ee26ae4a45c8e 0b14e887dc524d55affce5a3bd9b9f8d +#: 0cb557ccd8a6423091d0d562f1af31fd 0d1ef94754104f01a11302d72600e73a +#: 1475f9cb68fe417b9de7226406e810b5 14dcda8b059b480982e3fb03399e665b +#: 1d7731490c2f49959618924ddf4327fd 1e40a9184f8544c6b7a1472d55c66a11 +#: 24af9c4562134d16b50c664902b39dff 295718cd92e04380af38e7afaff04010 +#: 34fc7f7760d94c2f8bac262ff821b4d7 451b693e9c414cff9798a5a1592fe9cd +#: 47bbee4bd36c4f998fd04c8cc8d9c199 537ed27e714148da8ea804e3562ccd96 +#: 565b9def256a453784c749fe2ee93bc8 57dee3931014419c91d049fca185ef1b +#: 5dcfbd3c4b13401a82e19373602a4b92 635677a7aff64b79a4f69ffb4c22841c +#: 74b8a4ad713a43f2a14faf494e1886a4 7eca27378af64ce99374c606219ff337 +#: 7f6560a3959c4f069adff9d00ca33f54 855ad029af764579bf4ab14cbd430caa +#: 967377f151e84130a9778ece6228bec2 99035a24f6e94207b84545f8c71a451e +#: a90e5d6a51cd4591afeb3cd934071de0 c72891376fe2481d95c7d9ae3014ae47 +#: ca270cee6d3f4eb7ba331ab841c77f38 cb62202c244d4c08974c674c13a02dca +#: db47465f222c4f3db50a9133de7035f5 eb66bb58c7534346b105203e37b25760 +msgid "Returns" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:7 +#: f25d1962656c4f339805427e2573f84f of +msgid "" +"``(sender_id, payload)`` where ``sender_id`` is the sender's " +"``\"ip:port\"``, or None when the line is not a well-formed envelope." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:9 +#: d0d7af37e4134902ae64e10b2e43b4c1 of +msgid "``(sender_id, payload)`` where ``sender_id`` is the" +msgstr "" + +#: 09b385f0a98640c981d8563427e44d7e +#: PyFlow.network_api.connect_tcp.parse_forwarded_message:10 of +msgid "" +"sender's ``\"ip:port\"``, or None when the line is not a well-formed " +"envelope." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 0acb7e2291644625b8873e997a801b95 140c4a59eb0f43d5b4dc46fc2d3348a7 +#: 1412375394164f17aa5b40b7d9ecf0a4 19eb6fca79324b0791f8ec7488a5246c +#: 21c016ab955e4e6499a97f0ca86684cb 27bd312392a2404099fa44fd8787ed28 +#: 27dcdca3b6b247069ddce775ec471c70 28f7c9db48d949ef930abcedef021138 +#: 28fa8dcc5913460e8906e926c95408b9 2e363ed3ce6746c08ebfbf8bb889b623 +#: 3d654cd4b6b044baa3774f758d61da71 48e6af3ee30f47409e2837867a01ff48 +#: 50da78ed5f904c74a876d7b494241efa 50df226290614747b5568f633a883ae0 +#: 564af23329ae4b29aaea3a3638292f42 571f3b412784479a9bfdcb5034c6a39f +#: 6759afd1efe445adb4970c6265c95995 73e666bc0d164cb6a1506517d9acfdb4 +#: 752e44acb3104f9bbf4d3ff7c8bc244b 76862b0024bd42e2b012c999d92e6969 +#: 7ba45226839e40c8a00e684e7c4e07b9 7cd20d28b6794f34a0f53570fea9546a +#: afead614116b49619f1d29738a18e166 b443d67ba649468ca55f1889f18dd006 +#: bbc7b22bbf5e42f7bae149f481de9f3a c58a75656c6143f8a0cb397f24b619b2 +#: d4f12b17df174b038d554053e034cd2a e669e2ac035448fab6479e209ab51c4d +#: f278b3632fbc43f2b4cbeb7759606a68 f6ee270c707f4805b9035fa772078d19 +msgid "Return type" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:1 +#: f22254d8ca2d47ad8bc9c98664369d61 of +msgid "Split forward-command tokens into items and destination addresses." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:3 +#: b207f60051004834bab91ba392a88437 of +msgid "" +"A token of the form ``('ip', port)`` is a destination, everything else is" +" a forwarded item (message text or a path). Used by the native message " +"forwarding (``/forward_send_msg``) and by the file/folder forward " +"extension." +msgstr "" + +#: 5329fb72f5954768a6cec05ff4cccfea +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:7 of +msgid "Tokens after the command name." +msgstr "" + +#: 2aebf669048046ddb279a935bcfcfde4 +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:10 of +msgid "" +"``(items, addrs)`` in the order given; ``items`` holds texts and " +"paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: 96b96bd19fe94312bddf340fad073cbc +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:12 of +msgid "``(items, addrs)`` in the order given; ``items`` holds texts and" +msgstr "" + +#: 8e82954621d748e2b412952a7fb2753b +#: PyFlow.network_api.connect_tcp.parse_forward_items_and_addrs:13 of +msgid "paths, ``addrs`` holds ``(ip, port)`` tuples." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:1 +#: a51324658c7943158d90ed706cecc41d of +msgid "Build the console notice for a forward destination that cannot be served." +msgstr "" + +#: PyFlow.network_api.connect_tcp.forward_skip_message:3 +#: b541e8446e374bc8b38cd7912a1fa33c of +msgid "Destination ``(ip, port)`` that is unreachable or is the server itself." +msgstr "" + +#: 10e03f21d05e478d9cde0c7176f6e1b8 +#: PyFlow.network_api.connect_tcp.forward_skip_message:7 of +msgid "One-line notice for the console." +msgstr "" + +#: 8005e532fe9d491ca8ddd996889d605d +#: PyFlow.network_api.connect_tcp.parse_forward_originator:1 of +msgid "Extract the originator's ``\"ip:port\"`` from a received transfer command." +msgstr "" + +#: 2cf3052a89c645e782430f1f05fb37a1 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:3 of +msgid "" +"The server's forward relay tags every pushed ``/file`` and " +"``/file_folder`` command with the forwarding client's address tuple; a " +"direct send carries the receiver's own address instead." +msgstr "" + +#: 335f6c1b16104395ba38a33188943d8a +#: PyFlow.network_api.connect_tcp.parse_forward_originator:7 of +msgid "Received transfer command." +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:9 +#: e5bd1024ad3f47f19cafc4c83a844b60 of +msgid "" +"This instance's own ``\"ip:port\"``; a command carrying it is a direct " +"send and yields None." +msgstr "" + +#: 65469492f844448fab269b701cbdb704 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:13 of +msgid "" +"Originator ``\"ip:port\"``, or None when the command carries no " +"originator (direct send or non-transfer command)." +msgstr "" + +#: 04a0882a3a5e4099a114728b4d1c79c0 +#: PyFlow.network_api.connect_tcp.parse_forward_originator:15 of +msgid "Originator ``\"ip:port\"``, or None when the command carries no" +msgstr "" + +#: PyFlow.network_api.connect_tcp.parse_forward_originator:16 +#: a2d34276c4f54c69981f9b51f36cc31d of +msgid "originator (direct send or non-transfer command)." +msgstr "" + +#: 7297b41c47664243b9677b32e21de07e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:1 of +msgid "TCP server: accept clients, dispatch commands, relay messages and files." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:3 +#: f5119fd32b60425bb33e4aa148ff7139 of +msgid "" +"Each accepted connection is served by `handle_client` in its own thread: " +"a line starting with ``/`` goes to `handle_command` (built-in commands " +"plus the handlers registered with `register_command`), any other line is " +"a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``." +msgstr "" + +#: 2870b0163d68487daf7526e91eb49ff6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:11 of +msgid "Address the server socket binds to." +msgstr "" + +#: 007c3d5fac544fd480ac10cc56c2f236 0929a7309fef40cab69e9dc22c376ef2 +#: 2e995e9363c54a7ba267ff3a53e10908 507062a8930e432dbcab31b039724b27 +#: 66f4496879ee419d8d748faf8cff0f05 75fcffd8750942559c93c5e590a10503 +#: 83c05847c82b492090a9279f5072434d 8e64315489764ceba6e1120da4675ffb +#: 9c28c79f3bb84af98029a17c0ffc84cd +#: PyFlow.network_api.connect_tcp.TCP_Client_Base +#: PyFlow.network_api.connect_tcp.TCP_Server_Base +#: cf754e752b6c445989b3301b807fc9eb d61fb222783b4f898eb1e757631a1c9a of +msgid "type" +msgstr "" + +#: 2dcb98253fbf428b96fdb5b720769c02 568be916ae0340289a6569e718e7cbf2 +#: 904732a26bcc4835bd3429f0015ae8a4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:14 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:26 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:13 of +msgid "str" +msgstr "" + +#: 6ac74cca7b0f443ca89f3d2c3bbe1aaa +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:17 of +msgid "First port considered for binding and for allocation." +msgstr "" + +#: 0f9991451920470fa3c8a74228b46bb0 9ecf88c5193c470b98dc95d184c94f92 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:19 of +msgid "int" +msgstr "" + +#: 6746407aac194c4880c7cbf82b7fa2fb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:23 of +msgid "" +"Accepted connections keyed by ``(ip, port)``; each value holds " +"``socket``, ``address``, ``id`` and ``connected_time``." +msgstr "" + +#: 7ba2aaf3fe4c42739121eb3ca2bcc79a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:26 of +msgid "dict" +msgstr "" + +#: 93556efee2aa445684af3fdc1532548a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:30 of +msgid "True while the accept loop runs." +msgstr "" + +#: 0ab0667a38254203929e173ab5024f1d 76745b91cce345b092b4e5d931d71126 +#: 839584ae46a0488a95917b35e253768a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:38 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:44 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:32 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:38 +#: f692af2051ae43cdae389a97cd91e9d7 of +msgid "bool" +msgstr "" + +#: 4ad873e4e9da4574bc068029455f62ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base:36 +#: fae1c5fe3a844b69a2c7ec611073ecf9 of +msgid "Whether the RSA channel is negotiated." +msgstr "" + +#: 37e6dd691f544a80b9e0949883a2e5a8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:1 of +msgid "Create the server and, unless extended, start accepting clients." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:3 +#: bf58d54a167d45de9a18641877ada912 of +msgid "Address the server socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: 2f4d0c4bfd8246d4984d4c843510775d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:6 of +msgid "First port to bind; also the base of the allocation range." +msgstr "" + +#: 48b1e43c9f2a4e81a9c760ca0d80bad0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:8 of +msgid "Maximum concurrent clients. Defaults to 10." +msgstr "" + +#: 8786a9d2b6b449209be813778d785712 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:10 of +msgid "Step between candidate ports. Defaults to 1." +msgstr "" + +#: 15c0118df8da4454a7fea8d529989237 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:12 of +msgid "Number of ports per step. Defaults to 100." +msgstr "" + +#: 973382c423524d44a569c318399e0b99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:14 +#: d5f8f341d98d478289d9a226f37c1003 of +msgid "Concurrent file transfers allowed. Defaults to 10." +msgstr "" + +#: 4fe9a7607dbc4eebbce4e62f9306c76f +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:17 of +msgid "" +"Reserve a port range across processes, so several instances on one host " +"do not collide. Defaults to False." +msgstr "" + +#: 8aa56addc6ae4a7790dc957d1e1c2b60 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:20 of +msgid "Start the console command thread. Defaults to True." +msgstr "" + +#: 242566ff64584fba88e4fed8bb189110 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:28 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:23 +#: b6d5c80f7a8e4358889f1d90f9a579a4 of +msgid "" +"Worker slots for `submit_task` and threaded command handlers. Defaults to" +" 10." +msgstr "" + +#: 87d1cd7d6569498b8af8bc7064235784 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:26 of +msgid "" +"When True, do not call `start_TCP_Server`; the caller starts the server " +"when ready. Defaults to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:29 +#: ac43634ddb1a4d15a9f3d0f58d011441 of +msgid "" +"Negotiate the RSA-encrypted channel for every connection. Defaults to " +"True." +msgstr "" + +#: 1ffccca56902418ba98ee0c6776ad8fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:37 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:32 +#: e0eb36563dc5479a92ce7555eb5a7965 of +msgid "" +"``[pub_key_path, pvt_key_path]`` pair used instead of the default key " +"lookup; an invalid pair is ignored." +msgstr "" + +#: 420bbdf8d6bb400fbd3548bea03354ac +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:35 of +msgid "" +"Buffering ceiling in MiB for the in-memory forward pump; past it the " +"uploader is told to pause. Defaults to 2048." +msgstr "" + +#: ../../api/PyFlow.network_api.connect_tcp.rst +#: 012113b84d6e439db2b772b810838e0a 57443c71b0b84d20b3cf755ee7118632 +#: 63ab5a2f2327475ba324e245f8e6e2ac 7cfe1a15e78d4c2d91a46599dc106667 +#: 8b33b297e4e64f2ba7463207ef17cc68 a4cb07de3c4d4a5c959c403938ed51e6 +#: a85e930c77364c9eaac1c8c9e9e2c740 c79723467b3a4fa1a7ddcb1038fd8414 +msgid "Raises" +msgstr "" + +#: 37d7580e55904c278f12fe0f31fe0d3d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:46 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.__init__:39 +#: ed59ec69b9a1430597e6d8758051aff5 of +msgid "" +"If the ``.Flow`` directories or ``decode_command_table.json`` cannot " +"be created or read." +msgstr "" + +#: 810bc26336b24ad1acba3f56d466f8e9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:1 of +msgid "Reserve this server's port range under the cross-process lock." +msgstr "" + +#: 5540b6efb5864c188e080ec7aad5c3ca 9e68e81f7a184af8a8fe90557dbaa5de +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:3 of +msgid "No-op unless ``is_hand_alloc_port`` is True." +msgstr "" + +#: 356ed2e8333d49b98ed6834612b4b56d 4a28dc37e6b74154b3790646ec8abfa7 +#: 5e267cb8af8d49e38ddaeabd8a7e9b57 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:6 +#: daf116d5b2264b1a88c836c1ad5192a3 of +msgid "Step between candidate ports." +msgstr "" + +#: 28bfef7616a1498db2b8827758175f68 7e663e1ae24640d1b856b1f9a0525380 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.alloc_port:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:8 +#: e18848eedc5d4d8abd97d73a0984a5d3 f81918ab77304aeb99302710e3062db2 of +msgid "Number of ports per step." +msgstr "" + +#: 11e0523d1b194702a07a92748468da25 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.free_port:1 of +msgid "Release this server's reserved port range." +msgstr "" + +#: 801c063c5fa447bda1dbf7f5ccbc6a9a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the server port range for this process." +msgstr "" + +#: 672bb7a9496c43ac8b706a3d0062ddfd +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:1 +#: of +msgid "Report whether the server port range is reserved by some process." +msgstr "" + +#: 19c63440e9ff4dadb6a3614ef17ef055 39423e052b70403ba07a713aa9e3cb4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.is_server_port_temp_info_file_locked:3 +#: of +msgid "True while the lock file exists." +msgstr "" + +#: 5b6cf828ffb24e098a0c86a09818da73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.server_port_temp_info_file_unlock:1 +#: of +msgid "Remove the lock file that reserves the server port range." +msgstr "" + +#: 479bdf5518dc4a029ecf47ad44bdcd5d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:1 of +msgid "Allocate the next free server port range and record it on disk." +msgstr "" + +#: 34c085a1bfca4dedafc749bfdcf14ad5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:3 of +msgid "" +"``port`` is moved past the ranges already recorded by other servers, so " +"the instance ends up with a range of its own." +msgstr "" + +#: 197b30e318d741c2b1f8afc925e91e5c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_alloc_port:11 of +msgid "If the server port info file cannot be read or written." +msgstr "" + +#: 1c97c8ef484b42ab86d36b3742bf3879 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.hand_free_port:1 of +msgid "Drop this server's entry from the on-disk port range record." +msgstr "" + +#: 9fc07e857fa048e2956e64bf78f6e386 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:1 of +msgid "Allocate a transfer port, waiting until one is free." +msgstr "" + +#: 505d7b9fb1024bc8ba886e5b0a38fbf2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:3 of +msgid "" +"Allocated port, or 0 when allocation is disabled " +"(``is_hand_alloc_port`` False)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:5 +#: e85fdf7f7a594e0b9aed565b4a3ab42f of +msgid "Allocated port, or 0 when allocation is disabled" +msgstr "" + +#: 153357eb91794cb692467afe5e94b41c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.palloc:6 of +msgid "(``is_hand_alloc_port`` False)." +msgstr "" + +#: 26c5c3768e1446e78c9efc5f8038b23d 280a1814abb049cbb26998bd681609ec +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:1 of +msgid "Release a port obtained from `palloc`." +msgstr "" + +#: 0e0379de669843778f6827973c29372e 3ee789104c4749b1a5749730be83a046 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.pfree:3 of +msgid "Port to release." +msgstr "" + +#: 6a8d4efd9f9a45e399525e2f169035ee +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:1 +#: f9203c3ebcb24fd0b5110110b8693b9c of +msgid "Allocate the next port above the base, or the first free one in range." +msgstr "" + +#: 73e9e2ddd451402a93d10dbac6ac9374 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 9d96a242eab8433ca750d670a5f63b82 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:5 of +msgid "Allocated port; None when the upward range is exhausted; 0 when" +msgstr "" + +#: 7b05e18fccbe4ab081d3cd22d59a48e0 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_palloc:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:6 +#: ebba1d1151cc4d5991f783986cd0f480 of +msgid "allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 5f15d2ac36eb47a2b23fb93de93755c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:1 +#: f2d9abe435cf49f6825bb72a68013934 of +msgid "Release a port obtained from `file_palloc` and step the cursor back." +msgstr "" + +#: 6c1fb07a212d4bf19717407b0c7184d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.file_pfree:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:3 +#: c933ad6d68ec4c5d9bb5afc88c386fdd c9edcb78694a444fb0f2cc5f72db01fa +#: e32e165133154b9791e35f117f8a85a9 of +msgid "Port to release. Ignored when allocation is disabled." +msgstr "" + +#: 1afd81c685234a20aa41c5cde93d327d 7f5db505a5fa4c03a56e9ac5ff6aba74 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:1 of +msgid "Allocate the next port below the base, or the first free one in range." +msgstr "" + +#: 3fc63f6d51aa4c0fb22fd0f945f9cfd7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when " +"allocation is disabled (``is_hand_alloc_port`` False)." +msgstr "" + +#: 74a441ccfa5e421dadd215ef0725a25f 9b5089cb85494c069d408e344db75d9d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_palloc:5 of +msgid "Allocated port; None when the downward range is exhausted; 0 when" +msgstr "" + +#: 183b4335eebc41058759e30b97477018 9c4b9bddd98c463f9e140bd2d8425dad +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_pfree:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.spy_pfree:1 of +msgid "Release a port obtained from `spy_palloc` and step the cursor back." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:1 +#: c66c76cab8564da4b6b6cb855fd63fd5 df9eb5ed5e844a6a8f00e77be5c032e3 of +msgid "Register a custom command handler." +msgstr "" + +#: 85787fb3d66340eb9920983dd95e03c6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:3 +#: cdec2f24d2dc47bc8c505b7472dc3bf7 of +msgid "" +"Command to intercept, e.g. \"/my_command\"; matched case-insensitively " +"against the first token." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:6 +#: c7c29a3836c9493e9c8804cc5896144a of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back to the sender as the response." +msgstr "" + +#: 7c64d0c0b9ac44b995811a0fdaa0dfaf +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:10 of +msgid "" +"\"server\" for commands arriving from clients, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 7d2a7d91ad9743b29110a43100444097 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:13 +#: a0c074a9da664c5eb5fa63deb6765c36 of +msgid "" +"Run the handler on the worker pool instead of the reader thread. Defaults" +" to False." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:17 +#: ce0ad78e00b24bb8b675ce8d715b8323 e6c616f887334cb6925c03de2ad6f64c of +msgid "" +"False when ``where_to_run`` is neither \"server\" nor \"client\"; the" +" handler is then not registered." +msgstr "" + +#: 3a440bf70b36447e82577535a85baca1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:19 +#: f125f1386b434f3a8199b1ceb8c44eca of +msgid "False when ``where_to_run`` is neither \"server\" nor" +msgstr "" + +#: 1a68dac068db434eb76e28015005f29c 7c7784e8bc9f4a76b12b6be2d3df9285 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:20 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.register_command:20 of +msgid "\"client\"; the handler is then not registered." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:1 +#: ef5d20f1a16945368383a8cdbc3daa19 of +msgid "Register ``listener(client_id, message)`` for every inbound plain message." +msgstr "" + +#: 12cdf8300e934ace8498d00d39501ba2 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:3 of +msgid "" +"Plain messages are the lines received from clients that do not start with" +" ``/``; commands go through the registered command handlers instead." +msgstr "" + +#: 9ec5fafe8ded489785400e3e489b8573 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_message_listener:6 of +msgid "" +"``listener(client_id, message)`` where ``client_id`` is the sender's " +"``\"ip:port\"``. It runs on the receive thread, so it must not block, and" +" exceptions raised inside it are swallowed." +msgstr "" + +#: 3a640e40aeeb43938e65b49a2cd3dbba +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:1 +#: edb8edc2eb3f433186b54807a8ac43ad of +msgid "Unregister a listener previously added by `add_message_listener`." +msgstr "" + +#: 0336bd7428fa4f688cce21bb6a4156fe 6fd7bb75a29e4973ac2bd05119255039 +#: 977594c4b1074688958a018c03d9ee5b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_message_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_message_listener:3 +#: b59375740438476aa73d6f19c2240ce6 of +msgid "Listener to remove; an unknown one is ignored." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:1 +#: cf95477e5794404fbdf4e877a132ba1c of +msgid "" +"Register ``listener(client_id, full_path, name, size, command)`` per " +"saved file." +msgstr "" + +#: 9c57d09f6d3d437890ae3ad306071701 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:3 of +msgid "" +"Fired after a file uploaded by a client (a direct send, or a forwarded " +"file/folder item staged on the server) has been fully written to " +"``file_transfer_dir``." +msgstr "" + +#: 6c938b7c8be3464f9925527b6deb4e1d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.add_file_listener:7 of +msgid "" +"``listener(client_id, full_path, name, size, command)``; ``client_id`` is" +" the uploader's ``\"ip:port\"`` and ``command`` the wire command that " +"triggered the transfer, so a listener can recognise protocol pushes such " +"as ``/crypto_pub_key``. It runs on the transfer thread, so it must not " +"block." +msgstr "" + +#: 549734db517346ec814fa83ec2f46f00 57b1e98125974187b986386da7991dd0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.remove_file_listener:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.remove_file_listener:1 of +msgid "Unregister a listener previously added by `add_file_listener`." +msgstr "" + +#: 58f1ccd078ea406dab00d6d4be886a76 7a6c25f715d14b0aaf4ec7009db9fc3c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:1 of +msgid "Run a callable on the instance's worker pool." +msgstr "" + +#: 6e05c7f46b244ae195c32ee52797fa37 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:3 +#: a51df9d8fc924d468d8f6d8a61dfff12 of +msgid "Callable to run." +msgstr "" + +#: 6f9eee14b6cb4daf82acfbba1e34e873 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:5 +#: e1c79706c9414895bdf7bf183bf5d41a of +msgid "Positional arguments forwarded to ``func``." +msgstr "" + +#: 9221436fa83a4831942cf5be4e37e2cb 993bdb8fbe3240d3b1032014576182fe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:7 of +msgid "Keyword arguments forwarded to ``func``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:10 +#: a6b26d6cf54d4fafac2950cbe33ba56f df538e45ae7f4bd19398fd3d063a4744 of +msgid "" +"Handle for the submitted call; its worker slot is released when the " +"call finishes." +msgstr "" + +#: 214ffa9d5cb242d1b7a5f76c0da16cd6 60f19983f7384aefbb6c97b84dd25f93 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:12 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:12 of +msgid "Handle for the submitted call; its worker" +msgstr "" + +#: 942e2e90fc904344b71b0c2d9c39c9f6 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.submit_task:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.submit_task:13 +#: eadf6af0a0954798933dc427ba8e107e of +msgid "slot is released when the call finishes." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:1 +#: a2284b241a28498892e5a59158961b2e fece61298a354d6db119309b29c6751d of +msgid "Start a temporary listener for a side channel (not the main protocol)." +msgstr "" + +#: 01d42ba32c2b41f482dd077cfb950f9b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:3 +#: b7455f1055b7459ca8eee2b5052eb808 of +msgid "" +"``handler(client_socket, address)`` started in its own thread for every " +"accepted connection." +msgstr "" + +#: 5e1b84445ee34c9cac00e1a2c9ef297b +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:6 +#: c76bc24facf8407babf9023ef19c3879 of +msgid "Port to bind; None allocates one with `palloc`." +msgstr "" + +#: 4855e3ca5a3a41dfa37eb32a8d6c8d9c 9c69c4dd1d904078af48c6308ba35895 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:8 of +msgid "Listen backlog. Defaults to 1." +msgstr "" + +#: 29148f4a3160460f8dc27fd1e64c0a31 47f98fd15e6144bd845d175cd039a6c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:11 of +msgid "" +"``(port, thread, stop_event)``; setting ``stop_event`` ends the loop," +" which closes the socket and frees the port." +msgstr "" + +#: 91f803f6aa944d07a57cff61d1071efe +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:13 +#: ecf3d32c44db40a8a13545d623ffe2c9 of +msgid "``(port, thread, stop_event)``; setting ``stop_event`` ends the" +msgstr "" + +#: 6ca9f2a625634468a15171916ec6d6ea +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:14 +#: abce9546b7444c6383c9020c4133ec63 of +msgid "loop, which closes the socket and frees the port." +msgstr "" + +#: 04767fd6d11641ac88c41c78867609f5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_server:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_server:17 +#: a270cfabdac64296b8a33ad4d662896f of +msgid "If ``port`` is None and no port can be allocated." +msgstr "" + +#: 031d3e96394c454093561375a377dcce 7e534c863e4a4275adf9a3a93dd5cbc0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:1 of +msgid "Open a temporary outbound connection for a side channel." +msgstr "" + +#: 112fda54e1eb4981b4272df61c5416e2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:3 +#: f9f4af8e25654b4ca38700b9faf635ef of +msgid "Host to connect to." +msgstr "" + +#: 26edcce3a1c24d5d95ca0b2a86cdff36 59d3c414b4304673be5d3232d2291680 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:5 of +msgid "Port to connect to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:7 +#: c5766672f68f47c9908e68d2ed3be7f3 of +msgid "Local port to bind; None lets the OS choose." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:9 +#: bfc1b713e9fc4fe797e50c357f472e76 c0e20701686a4ea68dcce54795822508 of +msgid "``on_data(data, client_socket)`` called for every received chunk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:14 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:13 +#: c563d04fcd7a41f1a2aeace42974430a cbd0df6f67a64a5580227be38a10020a of +msgid "" +"``(client_socket, thread, stop_event)``; setting ``stop_event`` ends " +"the receiver thread." +msgstr "" + +#: 0e1b9679f3104a9caab601e6bc7c0905 4fbec161e0c84ef1ba95379314846158 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:16 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:15 of +msgid "``(client_socket, thread, stop_event)``; setting ``stop_event``" +msgstr "" + +#: 14528f9f11144ced9b5822d2640fe2a9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.create_temporary_client:16 +#: c76fce92c3f944c1857df89bbc134540 of +msgid "ends the receiver thread." +msgstr "" + +#: 5ea5fd66c3d3434292e4d8e1dd1da094 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:1 of +msgid "Send one message to every connected client." +msgstr "" + +#: 32945f04e0564f9d88495506f0ab4f90 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:3 of +msgid "Clients whose send fails are disconnected and removed from ``clients``." +msgstr "" + +#: 00de0b1f60b14189b1629f8146db68a6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:5 of +msgid "Payload passed to `send_message`." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.broadcast:7 +#: a91c8f6921c7435e990516c2134efcf8 of +msgid "``(ip, port)`` to leave out, typically the client the message came from." +msgstr "" + +#: 9d57c62f1fd94c1dacfe32aa50cc9665 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:1 +#: of +msgid "Send the messages of a console line to the clients named in it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_msg_to_specific_client:3 +#: e7fbf4bbd84b4920a2b9591fabcaf2d9 of +msgid "" +"``/send_msg`` line as typed: message text followed by one or more ``(ip, " +"port)`` identifiers; each message is delivered to the identifiers that " +"follow it. Addresses that are not connected are skipped with a console " +"notice." +msgstr "" + +#: 9d4819fe4ca94a1b81c9e0423ff98212 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:1 of +msgid "Write one line to a client socket, encrypting when the channel is up." +msgstr "" + +#: 1c7b88e29a0143a39e13441769186ef4 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:3 of +msgid "Target connection." +msgstr "" + +#: 2d32d96ad70e41d893b15bfa90c0864b 6f8bd9adaf2b4c44a9446b049144fe21 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:6 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:5 of +msgid "" +"Payload; a str is stripped and newline terminated, bytes are sent as they" +" are." +msgstr "" + +#: 5c03983cb460497fb72f126c1d8a2bc1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:9 of +msgid "" +"True when the payload was written, False for an unsupported payload " +"type." +msgstr "" + +#: 0b11a149117c4a1fb6bcf2fba3d1a515 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:11 of +msgid "True when the payload was written, False for an unsupported" +msgstr "" + +#: 602950a9f5ad48ea92ab60aea3b26488 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:12 of +msgid "payload type." +msgstr "" + +#: 149f767d72dc4da7b1d88fed768d2474 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:15 of +msgid "If the server is not running or no socket was passed." +msgstr "" + +#: 07dac2d20d7041779c8b189d45ee2e64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.send_message:16 of +msgid "If the socket write fails (the original error is re-raised)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:1 +#: caf2377ee7f348ab8c95bf2a27c44be4 of +msgid "Read up to ``msg_length`` bytes from a client socket." +msgstr "" + +#: 214af2667d0f443eb16c0a4aaf723b2e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:3 +#: cf94d4a352744a61a1f781128fae9694 of +msgid "Connection to read from." +msgstr "" + +#: 2b0cd669c9994324be7570e59a04677d 7c3eb31687804ba5a59acc140ba50230 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:5 of +msgid "Maximum number of bytes to read." +msgstr "" + +#: 51eade0145d74a2bbfc51070bc779703 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.receive_message:8 +#: ad9dfdd3ef56445181d7204b7b5bfa15 of +msgid "Received bytes, empty when the peer closed the connection." +msgstr "" + +#: 7103cfcec26c44999cb5464251ee461c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:1 of +msgid "Serve one accepted client until it disconnects." +msgstr "" + +#: 65f9de7f44774aa6aef81fcbe4dc1c64 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:3 of +msgid "" +"Registers the client, greets it, announces the encryption mode and reads " +"lines until the peer closes: commands go to `handle_command`, plain " +"messages go to the message listeners and to ``messages_dict``. Runs in " +"its own thread; the client is removed from ``clients`` and the socket " +"closed when the read loop ends for any reason." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:9 +#: db3915757e2842c8afee97b2d471411e of +msgid "Accepted connection." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_client:11 +#: fae378e888be4821a14ed2483cfde1e0 of +msgid "Peer ``(ip, port)``; used as the client id and as the key in ``clients``." +msgstr "" + +#: 32886e2babcf4283924034c600453c4a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:1 of +msgid "Dispatch one command line received from a client." +msgstr "" + +#: 8972b8c4edf149fd9a4008832c85ed31 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:3 of +msgid "" +"Built-in commands (``/help``, ``/time``, ``/clients``, ``/quit``, " +"``/crypto_mode``, ``/file``, ``/file_folder``, " +"``/server_file_transfer_port`` and the crypto exchange lines) are handled" +" here; any other name goes to the handlers registered for the \"server\" " +"side via `register_command`. An encryption-mode mismatch closes the " +"connection; an unknown command is only reported on the console." +msgstr "" + +#: 208294924333477a8e7d65aa3130777e +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:10 of +msgid "Connection the line came from." +msgstr "" + +#: 306e5026cfc1441d8c754f0e02866cf3 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:12 of +msgid "Peer ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:10 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:14 +#: a2a3a1fd57e14260a53675457fdff0b4 d87b7a9e04464b7ea4f979d46b42932b of +msgid "Line including its leading ``/``." +msgstr "" + +#: 8d727a2aaab14beaa1b6858b59b0821d +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:17 of +msgid "" +"Response for that client, or None when no response is due (crypto " +"lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:19 +#: ed04a3bf285b4f57bb6a5de0c4f7284f of +msgid "Response for that client, or None when no response is due" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.handle_command:20 +#: c4ca8e9c3e9941e1b33b5e0af2c13bab of +msgid "" +"(crypto lines, file transfers, and custom handlers that run in the " +"background)." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:1 +#: a784dfdc5ded411d985426e36f6e5435 of +msgid "Send one plain message to a connected target, tagged with its origin." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:3 +#: b545977244dc402aa8586264d2d933f3 of +msgid "" +"Public API for forward extensions: the message is wrapped in a " +"``/send_msg_from `` envelope so the receiver can " +"attribute it to the originator (see `parse_forwarded_message`)." +msgstr "" + +#: 8ee5275fbf894581ade9e6f30d5d4aff +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:7 +#: ee97948978e64b099947a0716f27d581 of +msgid "Destination ``(ip, port)``." +msgstr "" + +#: 38ffd8a7079d4794811e3504c01052f8 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:9 of +msgid "Payload to deliver." +msgstr "" + +#: 3b48d6e3b84b4dde9a09f3c2f07c8b61 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:15 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:13 +#: cf0f9590c44e40309471c70ebd2e433d e2207f0b24bf42c984505e0de8c8f8bf of +msgid "Originating client ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:14 +#: d2471a90e269464c977239f406051120 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the envelope was sent." +msgstr "" + +#: 1ae8a7060df7423895175f210441a399 320d99b8923f4eaa886ffe0a16bfc6d9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:24 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:16 of +msgid "False when ``target`` is not connected (a console notice is" +msgstr "" + +#: 1a97f59b270f404096d9c2b37ab09a8c +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_message_to:17 of +msgid "printed); True when the envelope was sent." +msgstr "" + +#: 6baab06dc12e4ecc80d1a16c26c2a19a +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:1 of +msgid "Build the tagged wire command that pushes one forwarded item." +msgstr "" + +#: 0c3f9dcbcf8f413fbd5befefb960c402 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:3 of +msgid "" +"Public API for forward extensions. The originator tuple sits before the " +"trailing transfer id, where the receiver's existing parsers ignore it and" +" `parse_forward_originator` recovers it for attribution." +msgstr "" + +#: 3cf58937a2d04172bf544119279d85c1 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:9 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:7 +#: a5ccb57e63d144c2844910f8ecf2faa4 of +msgid "\"file\" or \"file_folder\"." +msgstr "" + +#: 621a93603c8549c0ba047bee6aae5a38 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:11 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:9 +#: a20ab65e584c4daf9a9153008b713ee1 of +msgid "Relative folder path (folders only)." +msgstr "" + +#: 410039575afd4797a0eb1be9098ccda5 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:13 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:11 +#: f0f56fb642874c3889fb0dacfed4f4ef of +msgid "File or folder name." +msgstr "" + +#: 214d3e0a88fa41d8a02af0a5d09c5ac6 72d2b6fdd324482eaea9803def1fe2da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:17 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:15 of +msgid "Transfer id shared by the pushed item." +msgstr "" + +#: 0aa1bd5b053544e98ae4ab3ae1add5f7 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:19 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:17 +#: e7826b64fe174bf0a6f7faa4df49f491 of +msgid "Receiver-side destination directory." +msgstr "" + +#: 93e666db45e745619b90c744e1646b73 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_target_command:20 of +msgid "Command line to hand to `send_message`." +msgstr "" + +#: 56d8327bb7414a5e85292f8f2ace9d42 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:1 of +msgid "Push one forwarded file or folder item to a connected target." +msgstr "" + +#: 3098afa7f4ac43a184d081dda8faa6ba +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:3 of +msgid "" +"Public API for forward extensions: sends the line built by " +"`forward_target_command`, which the receiver attributes with " +"`parse_forward_originator`." +msgstr "" + +#: 9b9634061040430484cd2596f43a8feb +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:22 of +msgid "" +"False when ``target`` is not connected (a console notice is printed);" +" True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.forward_item_to:25 +#: b8bb8c432f7649d5a35480fec0509a51 of +msgid "printed); True when the command was sent." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:1 +#: b03f4edeca5f4cc19f6d695aac3690de of +msgid "Bind the server socket, then accept clients until `stop` runs." +msgstr "" + +#: 7e027ea659ab48e19ea346cb284fe144 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.start_TCP_Server:3 of +msgid "" +"Blocks the calling thread. A console command thread is started when " +"``is_input_command_in_console`` is True, every accepted connection gets " +"its own `handle_client` thread, and a client beyond ``max_clients`` is " +"refused with a message. Socket errors and the end of the accept loop both" +" end in `stop`." +msgstr "" + +#: 291f5a8cc4f648ee9910e5cd45074675 +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:1 of +msgid "Read console commands until the server stops." +msgstr "" + +#: 1ba2e428d432496688ef23f1c07562da +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.console_input:3 of +msgid "" +"Handles ``/stop``, ``/status``, ``/clients``, ``/send_msg``, ``/file``, " +"``/file_folder``, ``/multiple_file_multiple_client``, " +"``/diff_multiple_file_diff_multiple_client`` and ``/help``; the forward " +"commands are client-only and are refused here. Any other name goes to the" +" handlers registered with ``where_to_run=\"client\"``. Ctrl-C and EOF " +"stop the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:1 +#: eb90f49c77e64006a0c9ae861ff849b1 of +msgid "Stop the server and release everything it owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Server_Base.stop:3 +#: a059b1e71106487999c83c2e2042f509 of +msgid "" +"Closes the server socket and every client connection, flushes the message" +" and event stores, releases the allocated port range and clears " +"``running``. Safe to call more than once." +msgstr "" + +#: 32b06ea315b3404caa3a05c4aed0ce64 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:1 of +msgid "" +"TCP client: connect to a server, dispatch commands, send and receive " +"messages." +msgstr "" + +#: 2b30f5c9ecd24fc89cdcd58000ee1e99 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:3 of +msgid "" +"Lines received from the server go through `receive_messages`: a line " +"starting with ``/`` is handled by `handle_server_command` (protocol " +"commands plus the handlers registered for the \"server\" side), any other" +" line is a plain message delivered to the listeners registered with " +"`add_message_listener` and stored in ``messages_dict``. With " +"``is_input_command_in_console`` the console thread `interactive_mode` " +"sends typed lines to the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:12 +#: b9532c86786c43d5aea58b24157fefed of +msgid "Server address this client connects to." +msgstr "" + +#: 2cfa1e3e1674407ba1e2683e76fef65d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:18 of +msgid "Server port this client connects to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:24 +#: af04fdee2c95431a9b0c17f96e50c18c of +msgid "Local address the socket binds to." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:30 +#: b1d4b5606e1c48b6a915815527542758 of +msgid "Local port, None when the OS chose one." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:32 +#: d926b4f3eb4b40a4acb21f780a4dc227 of +msgid "int | None" +msgstr "" + +#: 500cfd177cf5453d887f6904b73b7851 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base:36 of +msgid "True while the connection is up." +msgstr "" + +#: 40f2a9e1b730422db3dd58bc2a6046e3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:1 of +msgid "Create the client and, unless extended, connect and start reading." +msgstr "" + +#: 0552aa36b7b74474af8f43b380dbee54 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:3 of +msgid "Server address to connect to; required before `connect` is called." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:6 +#: d246ff9aaf154bfa91b5c4c9d293afd5 of +msgid "Local address the socket binds to. Defaults to \"127.0.0.1\"." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:9 +#: e981b68ebbea46e9b556b09f4b5cd44a of +msgid "Server port. Defaults to 65432." +msgstr "" + +#: 766bf4c138234fe081bbd2668dcb2a5d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:11 of +msgid "Local port to bind; None lets the OS choose an ephemeral port." +msgstr "" + +#: 5d661c0f91f64ef39cacb1a5466d752c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:14 of +msgid "" +"Socket timeout in seconds for connect and receive. Must be None when " +"``is_wait_server`` is True." +msgstr "" + +#: 9478d02062284ecaa7838f931ab80780 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:17 of +msgid "Step between candidate ports in the allocation range. Defaults to 1." +msgstr "" + +#: 2e1695ae510b43758793257124fd23c2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:22 of +msgid "Enter interactive mode after connecting. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:25 +#: e02da86957dd4f45bf84ede4e9c82cda of +msgid "Keep retrying while the server is not reachable. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:31 +#: e1fb7f67e1714a61a9e8fe500555c700 of +msgid "" +"When True, do not call `start_TCP_client`; the caller connects when " +"ready. Defaults to False." +msgstr "" + +#: 1634dbe84a95421a92a660f7c6fdd10e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:34 of +msgid "Negotiate the RSA-encrypted channel with the server. Defaults to True." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:40 +#: cff66a7ac1ad410f96ed1ee03cb1cf53 of +msgid "" +"Buffer ceiling in MiB, kept for parity with the server class; the " +"client's forward path does not read it today. Defaults to 2048." +msgstr "" + +#: 0f06ff3b6db940da96ec09bee3dba526 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.__init__:45 of +msgid "If ``is_wait_server`` is True and ``timeout`` is not None." +msgstr "" + +#: 5dd677b9627d4b84b53dc7a6b2848b3f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:6 of +msgid "" +"``handler(client_socket, client_address, command)`` called with the raw " +"line; a non-None return value is sent back as the response." +msgstr "" + +#: 0a57a7875bb94767904ff8d93fa77eb8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.register_command:10 of +msgid "" +"\"server\" for commands pushed by the server, \"client\" for commands " +"typed on this instance's console." +msgstr "" + +#: 876e8fe2a6d645829ba12ddbad5006df +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:1 of +msgid "Register ``listener(sender_id, message)`` for every inbound plain message." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:3 +#: d166a2cc8ea04bed82c5f6dd61dcdcf6 of +msgid "Mirrors the server-side contract; commands are not reported here." +msgstr "" + +#: 412834182c9442adaf0b85e24c200ccc +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_message_listener:5 of +msgid "" +"``listener(sender_id, message)``; ``sender_id`` is the author's " +"``\"ip:port\"`` — the forwarding client for a message another client " +"forwarded here (``/send_msg_from`` envelope), or None for a direct push " +"from the server, which names no client author. It runs on the receive " +"thread, so it must not block, and exceptions raised inside it are " +"swallowed." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:1 +#: fc8a83e0b5f84273a8287b122c1a9a3e of +msgid "" +"Register ``listener(full_path, name, size, command)`` per saved inbound " +"file." +msgstr "" + +#: 34e3386853564aab9a824e96438fc3e5 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:3 of +msgid "" +"Fired after a file pushed by the server (a direct send, or a forwarded " +"file/folder item) has been fully written to ``file_transfer_dir``." +msgstr "" + +#: 63f925709b83433e967610672b6fbc79 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.add_file_listener:6 of +msgid "" +"``listener(full_path, name, size, command)``; ``command`` is the wire " +"command that triggered the transfer, so a listener can recognise protocol" +" pushes such as ``/crypto_pub_key``. It runs on the transfer thread, so " +"it must not block." +msgstr "" + +#: 0e91f82d8856443497c399f97643b757 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:1 of +msgid "Reserve this client's port range under the cross-process lock." +msgstr "" + +#: 6029a28126344e45b24394cb56b2f8f8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.alloc_port:3 of +msgid "No-op until the server assigns a range (see ``/client_alloc_port_range``)." +msgstr "" + +#: 32bed401c3574959ba5ed08fc9078401 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:1 of +msgid "Release this client's reserved port range." +msgstr "" + +#: 9f86b7aac4f44886be9cd87b56072b63 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.free_port:3 of +msgid "No-op unless a range was assigned (``is_hand_alloc_port`` True)." +msgstr "" + +#: 8154f96d9ec74bd588971a806d42f8c1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_lock:1 +#: of +msgid "Create the lock file that reserves the client port range for this process." +msgstr "" + +#: 69c2dd5d5d1d497c8bac1e62ab4fc30a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.is_client_port_temp_info_file_locked:1 +#: of +msgid "Report whether the client port range is reserved by some process." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.client_port_temp_info_file_unlock:1 +#: c88f37ef2d5c47bda346e804508d6ec0 of +msgid "Remove the lock file that reserves the client port range." +msgstr "" + +#: 478443e187814a6ca7ad798277db023f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:1 of +msgid "Allocate the next free client port range and record it on disk." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:3 +#: a8736540f4c3402fad9508e619d559d7 of +msgid "" +"``port`` is moved past the ranges already recorded by other clients on " +"this host, so each instance ends up with a range of its own." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_alloc_port:11 +#: bc44b1604e02467280b59cb8ad63d0af of +msgid "If the client port info file cannot be read or written." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.hand_free_port:1 +#: e48dff576a074109a135db168b5dbe98 of +msgid "Drop this client's entry from the on-disk port range record." +msgstr "" + +#: 3aa62e8843c6495c862f43fc6dda9d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:1 of +msgid "Allocate a port, waiting until one is free." +msgstr "" + +#: 2f7752fa51d946b1a4a566b38f046e80 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.palloc:3 of +msgid "Allocated port, or 0 when no allocation range was assigned." +msgstr "" + +#: 503ab40fc32c4d548f65fa3272b2f4d2 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:3 of +msgid "" +"Allocated port; None when the upward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:5 +#: b9fd549c95f24c2080d87cec92514db6 of +msgid "Allocated port; None when the upward range is exhausted; 0 when no" +msgstr "" + +#: 151a65f1c1984e4196c750108d0611cb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.file_palloc:6 of +msgid "allocation range was assigned." +msgstr "" + +#: 0c6201bb2d7e45d3a4916d7e50a21e10 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:3 of +msgid "" +"Allocated port; None when the downward range is exhausted; 0 when no " +"allocation range was assigned." +msgstr "" + +#: 76c766a9c61f4075a762ccf55038a65e +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.spy_palloc:6 of +msgid "no allocation range was assigned." +msgstr "" + +#: 8d500513e8854ba4bb91c8905a9adb73 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.create_temporary_client:7 of +msgid "Local port to bind; None allocates one with `palloc`." +msgstr "" + +#: 840360623c8a4c2bbafe8fe7bbf9209c +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:1 of +msgid "Connect to the server and start reading from it." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:3 +#: df94288e660b48f6809358b3dc234ec9 of +msgid "" +"Binds ``client_port`` when one was configured, then retries while " +"``is_wait_server`` is True and the server is not reachable yet. Once the " +"socket is up the receive thread is started and the encryption mode is " +"negotiated, which closes the connection when the two sides disagree." +msgstr "" + +#: 22fccba0ab36499a9a7f4427abe5e9f4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:8 of +msgid "" +"True when the connection is established (and, if encryption is " +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 0270d0a2d42740a9889106339d9f2dbb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:10 of +msgid "True when the connection is established (and, if encryption is" +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.connect:11 +#: c194691cedfb48c6bfd97d9bdc6f2246 of +msgid "" +"enabled, the key exchange has been started); False when the attempt " +"failed or the mode negotiation closed the connection." +msgstr "" + +#: 4dc38e3ef3b74b8e8c27378242a1925a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:1 of +msgid "Read from the server until the connection ends." +msgstr "" + +#: 431649a0d703476dadc6afa8adbe53d4 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_messages:3 of +msgid "" +"Runs on the receive thread: plain lines are reported to the message " +"listeners and stored in ``messages_dict`` (``/send_msg_from`` envelopes " +"are attributed to their sender first), other ``/`` lines go to " +"`handle_server_command`. Any end of the connection clears ``running`` and" +" releases the port range." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:1 +#: cf284b59f7f849039404ced20f8e12db of +msgid "Write one line to a socket, encrypting when the channel is up." +msgstr "" + +#: 40ba2ede2d3f4666b82107af129d24d0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:3 of +msgid "Target connection; the client passes ``self.client_socket``." +msgstr "" + +#: 2e663941eaf142cb96738add05f9ef4d +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:10 of +msgid "" +"True when the payload was written; False when the client is not " +"running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: 38905ec7539d42e0839907c32dd8667f +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:12 of +msgid "True when the payload was written; False when the client is not" +msgstr "" + +#: 3f7e2e6fedda476f996132c3f677f2f0 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message:13 of +msgid "running, no socket was passed, or the payload type is unsupported." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:1 +#: a9220a130e694e0aba1d3b6320dff498 of +msgid "Send the payload of a console line to the server." +msgstr "" + +#: 2e208e83a89e4dac9793460a8808a042 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:3 of +msgid "" +"Console line such as ``/send_msg hello``; the first token (the command " +"name) is dropped and the second one is sent." +msgstr "" + +#: 39c8e70ca92b43b8ac687bc0aa7f4073 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.send_message_to_server:7 of +msgid "If the line has fewer than two tokens." +msgstr "" + +#: 8d47f30ae7b0451f8ecfe207ec835ff3 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.receive_message:1 of +msgid "Read up to ``msg_length`` bytes from a socket." +msgstr "" + +#: 10a39005f45c46728ef9a000eeaf9109 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:1 of +msgid "Dispatch one command line pushed by the server." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.handle_server_command:3 +#: a7e781fde939475cb081204adac16ded of +msgid "" +"Handles the protocol's own lines: ``/crypto_mode`` (a mismatch closes the" +" connection), ``/client_alloc_port_range``, the ``/crypto_*`` exchange " +"lines, and the transfer lines ``/file``, ``/file_folder``, " +"``/forward_upload``, ``/pause_trans``, ``/start_trans``, " +"``/forward_error``. Any other name goes to the handlers registered for " +"the \"server\" side via `register_command`; an unknown command is only " +"reported on the console." +msgstr "" + +#: 91942d88960c4793b5eab8c3654c1400 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:1 of +msgid "Forward plain messages to other connected clients through the server." +msgstr "" + +#: 75bb9147a7464a278e19af9d03b04ecb +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:3 of +msgid "" +"The console command ``/forward_send_msg`` uses this; the client must be " +"connected. The server wraps each message in a ``/send_msg_from`` envelope" +" so the receiving client can attribute it back to this one." +msgstr "" + +#: 0f3c201936df45b687dc5532531b4a4a +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:7 of +msgid "Message texts to forward." +msgstr "" + +#: 277bbcb2143943b5b983badb00c3f4fa +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:9 of +msgid "Destination ``(ip, port)`` tuples." +msgstr "" + +#: 5ea8eea489fc43108a439d54fba31068 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:12 of +msgid "" +"True when the request was written to the server; False when the " +"client is not connected." +msgstr "" + +#: 2bc5d7e5dfa94eb29b49f12562f57c88 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:14 of +msgid "True when the request was written to the server; False when the" +msgstr "" + +#: 45d974ad946b408b91d41bcce65abcc8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_messages:15 of +msgid "client is not connected." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:1 +#: c9401da7c37e40e5978c80e4876b11a0 of +msgid "Read console lines and act on them until the client stops." +msgstr "" + +#: 793bcfd02c014200b0ec47f8e15b4d91 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.interactive_mode:3 of +msgid "" +"``/quit`` closes the connection; ``/send_msg``, ``/file``, " +"``/multiple_file``, ``/file_folder``, ``/multiple_file_folder``, " +"``/forward_file``, ``/forward_folder`` and ``/forward_send_msg`` are " +"handled locally; any other name goes to the handlers registered with " +"``where_to_run=\"client\"``, and anything left is sent to the server as " +"it stands. Ctrl-C and EOF close the connection." +msgstr "" + +#: 6dbae803a56c442588b28af664b7a0e9 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_file_console:1 of +msgid "" +"/forward_file ... ... [dest] (client " +"only)." +msgstr "" + +#: 05cad8b03e0747dc804ffbfda3122fe1 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.forward_folder_console:1 of +msgid "" +"/forward_folder ... ... [dest] " +"(client only)." +msgstr "" + +#: 6e9a9717a782419baecca63fd1f46baf +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:1 of +msgid "Close the connection and release everything the client owns." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.close:3 +#: ba6c5badecf6465db6036735870ed305 of +msgid "" +"Stops the receive loop, releases the port range, flushes the message and " +"event stores and closes the socket. Safe to call more than once." +msgstr "" + +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:1 +#: c91a3245b93f46abbb49dbef7bcfeb1c of +msgid "Connect to the server and start the client loop." +msgstr "" + +#: 0c83552867484ea780fcff8d34e5c2d8 +#: PyFlow.network_api.connect_tcp.TCP_Client_Base.start_TCP_client:3 of +msgid "" +"Enters `interactive_mode` when ``is_input_command_in_console`` is True, " +"otherwise keeps the process alive while the connection is up. Exits the " +"process with status 1 when the connection cannot be established; Ctrl-C " +"and the end of the connection both run `close`." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po new file mode 100644 index 0000000..380ad68 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.connect_udp.po @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.connect_udp.rst:2 +#: c777b065237a4eb1993c041388639a1d +msgid "PyFlow.network\\_api.connect\\_udp module" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.po new file mode 100644 index 0000000..c1e422a --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rst:2 9abd125493904de2bb64c9158a11243f +msgid "PyFlow.network\\_api package" +msgstr "" + +#: ../../api/PyFlow.network_api.rst:10 7012029060714904ba5d281c1d607be9 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po new file mode 100644 index 0000000..114d4bf --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.network_api.rsa_crypto.po @@ -0,0 +1,232 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.network_api.rsa_crypto.rst:2 +#: def9740b5a2e4aaeb2aeed8b4f02c0ec +msgid "PyFlow.network\\_api.rsa\\_crypto module" +msgstr "" + +#: 46d3090efafd4504870cf72a4bea22ab PyFlow.network_api.rsa_crypto:1 of +msgid "crypto_api (C/OpenSSL) RSA integration for PyFlow's TCP layer." +msgstr "" + +#: 5b1ff4a00b0f46258cda2e7440368de1 PyFlow.network_api.rsa_crypto:3 of +msgid "" +"A thin ctypes binding to the shared ``libcrypto_api`` plus the key " +"lifecycle required by the encrypted TCP channel:" +msgstr "" + +#: PyFlow.network_api.rsa_crypto:6 a5a0d6d600604683990a92555a49a3fe of +msgid "" +"Reuse an existing RSA keypair from ``~/.ssh`` (PEM private key) when one " +"is present and parseable, otherwise generate a fresh keypair into " +"``.Flow/pvt_key``. A caller-supplied keypair (``custom_keys``) is " +"honoured when both files parse and the pair matches." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:10 bb413dc908624589a3e57f8a2c702728 of +msgid "" +"Anti-MITM identity check (TOFU): every connection exchanges public keys " +"in plaintext. Each side records the peer in " +"``.Flow/pub_key/pub_key.json`` under the peer's ``(ip, port)`` with the " +"SHA-256 of its public key; a later connection from the same endpoint " +"presenting a different key is rejected, and a known key seen from a new " +"endpoint is re-registered under the new ``(ip, port)``." +msgstr "" + +#: PyFlow.network_api.rsa_crypto:16 aec1a7998276428881efcfac6fe4cebe of +msgid "" +"RSA-OAEP encrypt/decrypt with the ``_VALID`` plaintext signature so a " +"stale key (for example a rotated ``~/.ssh`` pair) is detected and the " +"peers re-exchange their public keys." +msgstr "" + +#: 60edbdead17c422782e688ab68f8a6d1 PyFlow.network_api.rsa_crypto:20 of +msgid "" +"The C library must be built first (``cmake -S . -B build && cmake --build" +" build``); see ``load_library`` for the search paths." +msgstr "" + +#: 67af7d5c5f41474fba915352e506a8b4 +#: PyFlow.network_api.rsa_crypto.CryptoLibraryError:1 of +msgid "Raised when the shared libcrypto_api cannot be loaded." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaKey:1 e620122ff0bc48dd985c4b6a0b3b41ce of +msgid "Owns an ``pf_rsa_key_t*`` handle; frees it on GC." +msgstr "" + +#: 1a7d8e9db5a3435486d716b13f2a2a56 +#: PyFlow.network_api.rsa_crypto.load_library:1 of +msgid "Locate and load the shared crypto_api library (cached)." +msgstr "" + +#: 670f4a719bf24c24b48ecb5d73b373ce +#: PyFlow.network_api.rsa_crypto.get_local_mac:1 of +msgid "Return a stable 48-bit machine identifier as colon-separated hex." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.get_local_mac:3 +#: f1306bb875d34b558f7418278fded413 of +msgid "" +"Uses ``uuid.getnode()`` (the real hardware MAC when one is available). " +"Server and client on the same host share this value; the ``_`` " +"prefix in the key file names keeps them apart." +msgstr "" + +#: 333763d798c54d52a9a56a3e4e3e2155 PyFlow.network_api.rsa_crypto.RsaCrypto:1 +#: of +msgid "Key lifecycle plus RSA-OAEP encrypt/decrypt for one role." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto:3 f52515121ac74575aaa36d45dd0341a2 +#: of +msgid "" +"``role`` is ``\"server\"`` or ``\"client\"`` and is used to name the " +"locally generated keypair (``pvt_key/_priv.pem``) and the peer key " +"cache (``pub_key/__.pem``). Peer identity is tracked" +" in ``pub_key/pub_key.json`` (TOFU, see ``verify_peer_pub``)." +msgstr "" + +#: 56fed5bcfe194b7f90a0c662ae36d6c0 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:1 of +msgid "Create the crypto wrapper for ``role`` (\"server\" or \"client\")." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.__init__:3 +#: ca4e27664ccf40ce935d991db43f7c69 of +msgid "" +"``custom_keys`` may be a ``[pub_key_path, pvt_key_path]`` pair to use a " +"user-supplied RSA keypair instead of the default lookup (``~/.ssh`` / " +"generated). The pair is validated on first use (paths exist, files parse," +" the keys match); an invalid pair is ignored and the default lookup is " +"used instead." +msgstr "" + +#: 7d89122ff0f54090b67209e3b08ae29c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:1 of +msgid "Load the RSA keypair (see module docstring) and cache handles." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.ensure_keys:3 +#: d5e494e00be44891b33cb8e0ecfb8081 of +msgid "" +"Runs under ``_key_lock``: the private-key handle must never be replaced " +"(or freed on GC) while another thread is decrypting." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.reload_own_key:1 +#: f6f7d6ec8837464dad2dc647e6141a99 of +msgid "Re-read the private key (e.g. after a ~/.ssh rotation)." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:1 +#: e95d18da34d14376987a62f6cbbec778 of +msgid "" +"Path of the exchanged public key file for ``peer_role`` at ``(peer_ip, " +"peer_port)``." +msgstr "" + +#: 52dc6fb74f0c4175bfa2a2acc23cda0d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.peer_pem_path:4 of +msgid "" +"The IP is sanitized for the filesystem (``:`` -> ``_`` so IPv6 literals " +"are safe on every platform, including Windows)." +msgstr "" + +#: 731d92a1b64342dfbf7c467af0a4c00e +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:1 of +msgid "TOFU check-and-record for a peer public key." +msgstr "" + +#: 634283c14fb44424bf148a34d156fb71 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:3 of +msgid "" +"``peer_pem`` is the PEM text received on this connection, ``(peer_ip, " +"peer_port)`` the endpoint it came from. Returns ``(ok, reason)``:" +msgstr "" + +#: 6bb58c88f8454d7daca92b5a478ae788 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:7 of +msgid "" +"key already registered under any endpoint -> accept, and re-register it " +"under the current endpoint when it moved (IPs are dynamic and ports are " +"user-changeable);" +msgstr "" + +#: 74e2bcc89bae4fe5aecf99e6208e21d1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:10 of +msgid "" +"key unknown but the endpoint already holds a *different* key -> reject (a" +" trusted endpoint suddenly presenting a new key);" +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.verify_peer_pub:12 +#: f856b875adc34f4a80c2b21cd506b143 of +msgid "" +"key and endpoint both unknown -> accept and record (first connection is " +"trusted, TOFU)." +msgstr "" + +#: 556fdf46d6c543e0ab06e2d4fabad7f1 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:1 of +msgid "Move a freshly received public key file into the key cache." +msgstr "" + +#: PyFlow.network_api.rsa_crypto.RsaCrypto.store_peer_pem:3 +#: a47e84f5d21949bf98e24834f18bed76 of +msgid "" +"Idempotent under concurrency: several transfers may deliver the same peer" +" key at once (multi-connection handshakes, several client processes " +"sharing one ``received_files/`` directory); if the source is already gone" +" because a concurrent store moved it, success is assumed when the " +"destination is in place." +msgstr "" + +#: 135944d28da54948862e515eb828072d +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:1 of +msgid "Encrypt ``plaintext`` with the peer's public key file." +msgstr "" + +#: 4b4dded0ad904ce2bb87c9a43c8f87e5 +#: PyFlow.network_api.rsa_crypto.RsaCrypto.encrypt_for_peer:3 of +msgid "" +"Returns the ASCII wire body (no trailing newline): each chunk is RSA-OAEP" +" encrypted and base64 encoded, chunks joined with ``|``. Raises if no " +"peer key is stored at ``peer_pem_path`` yet. The whole encryption runs " +"under ``_peer_pub_cache_lock`` so the peer handle cannot be freed mid-" +"encrypt (no-GIL safe)." +msgstr "" + +#: 8f1687f4ab984c029859fd1f9cfb968c +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:1 of +msgid "Decrypt a wire body with our private key." +msgstr "" + +#: 8a1d1504bf4248fa9e1288776d19810b +#: PyFlow.network_api.rsa_crypto.RsaCrypto.decrypt_with_own:3 of +msgid "" +"Returns ``(True, plaintext)`` on success, or ``(False, None)`` when the " +"key is stale/wrong or the ``_VALID`` signature is missing. Runs under " +"``_key_lock`` so the handle cannot be freed by a concurrent " +"``reload_own_key`` (no-GIL safe)." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.po new file mode 100644 index 0000000..2871216 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.rst:2 738ded08e11742acb4854652f885aa13 +msgid "PyFlow package" +msgstr "" + +#: ../../api/PyFlow.rst:10 8a3a28b0487c4bacb81f0afaa1b2902e +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.rst:19 28ca6a63167d49798f21d2796d6acf1e +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.po new file mode 100644 index 0000000..34f4071 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.po @@ -0,0 +1,33 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.rst:2 e8518825f60e48f391b84d3fb415bb36 +msgid "PyFlow.transfer\\_web package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:10 8d8e622c8bdd4618b0e33d94e1000e42 +msgid "Subpackages" +msgstr "" + +#: ../../api/PyFlow.transfer_web.rst:19 541ef21ed94743f2b9e11aadbde918b4 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po new file mode 100644 index 0000000..ff567a0 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_client.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_client.rst:2 +#: 09f60d1b11ac4e92b480fa6284489970 +msgid "PyFlow.transfer\\_web.setup\\_client module" +msgstr "" + +#: 2f786e174934474a93c6498878a68612 PyFlow.transfer_web.setup_client:1 of +msgid "PyFlow TCP client web launcher." +msgstr "" + +#: 3e6b6a90f3e7484f8a0ca766f8305082 PyFlow.transfer_web.setup_client:3 of +msgid "" +"Starts a lightweight Flask backend on 127.0.0.1 and opens the connect UI " +"in the browser. The user enters the server address (an http/https domain" +" or a bare IP); the backend asks the server's web backend for the TCP " +"server address/port, starts the TCP client, and keeps the backend running" +" to relay the user's frontend actions." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po new file mode 100644 index 0000000..f6825cf --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.setup_server.po @@ -0,0 +1,52 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.setup_server.rst:2 +#: e6642bb55a204188b0abd0b07207713e +msgid "PyFlow.transfer\\_web.setup\\_server module" +msgstr "" + +#: 5966876a4564470ea674311ddb3ef63e PyFlow.transfer_web.setup_server:1 of +msgid "PyFlow TCP server web launcher." +msgstr "" + +#: 588fea0dc68a4a308f9a7cfb6fe5d819 PyFlow.transfer_web.setup_server:3 of +msgid "Checks ``transfer_web/.Flow_Web/setup_server.json``:" +msgstr "" + +#: PyFlow.transfer_web.setup_server:5 c3df4df524da40b18a58607efd9b5e4a of +msgid "" +"missing -> opens the server startup-configuration UI in the browser; the" +" UI saves the config (same shape as ``flow_setup``'s ``setup.json``) and " +"starts the TCP server class;" +msgstr "" + +#: 211850fce6d34d50ae152bce6d1aa3af PyFlow.transfer_web.setup_server:8 of +msgid "present -> starts the TCP server class directly from the saved config." +msgstr "" + +#: PyFlow.transfer_web.setup_server:10 cb510f0a77474eff8e8816fafc097343 of +msgid "" +"After the TCP server is up, the lightweight Flask backend serves the " +"status page and the client-facing API (``/api/server_info`` etc.) on the " +"server's address." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po new file mode 100644 index 0000000..425faac --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:2 +#: 76090548cb6a4066a3558553adad502a +msgid "PyFlow.transfer\\_web.web\\_backend package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_backend.rst:10 +#: 0d60b135791b45019583e72be4a02afc +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po new file mode 100644 index 0000000..58b20f9 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_backend.server_backend.po @@ -0,0 +1,119 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_backend.server_backend.rst:2 +#: 4e16e69f9ae9491a95bfcab9c70cfe24 +msgid "PyFlow.transfer\\_web.web\\_backend.server\\_backend module" +msgstr "" + +#: 15eb6d4006b3429a8fa230a2b460b1c5 +#: PyFlow.transfer_web.web_backend.server_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP server for the web tool." +msgstr "" + +#: 3c413c9e37f94c7485dba6d4f96b9bf2 +#: PyFlow.transfer_web.web_backend.server_backend:3 of +msgid "Two modes, one process:" +msgstr "" + +#: 557b793ac8e441db86cf59cad5e85371 +#: PyFlow.transfer_web.web_backend.server_backend:5 of +msgid "" +"``config`` mode: serves the server startup-configuration UI. The UI " +"shows every ``TCP_Server_Base`` parameter with its default value; on " +"submit the config is written to ``.Flow_Web/setup_server.json`` (same " +"shape as ``flow_setup``'s ``setup.json``) and the TCP server class is " +"started." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:10 +#: cb922669795b42c6ad5587b527c4356a of +msgid "" +"``status`` mode: serves the minimal status page plus the same " +"sidebar/input UI as the client frontend (forwarding disabled; native " +"sends to connected clients allowed). Also exposes the HTTP API that " +"clients use to discover the TCP server address/port." +msgstr "" + +#: 0c6784517a194d65926c71b7ce7f4836 +#: PyFlow.transfer_web.web_backend.server_backend:15 of +msgid "" +"The backend monitors ``server.clients``: whenever a client connects or " +"disconnects it broadcasts the current instance list to every connected " +"client (``/web_clients_update``), and it re-checks the list every minute." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend:20 +#: e902a63f173f4e8ba20923c4d8f4b083 of +msgid "" +"Inbound events (plain-text messages and file uploads arriving from " +"clients) are captured on the TCP server's receive threads through " +"``TCP_Server_Base``'s ``add_message_listener``/``add_file_listener`` " +"APIs, queued here, and polled by the frontend via ``/api/events``." +msgstr "" + +#: 30582f045fae468cb63a543324edfea8 +#: PyFlow.transfer_web.web_backend.server_backend:25 of +msgid "" +"Authentication: anonymous visitors get a white landing page (the server " +"addresses plus a login button); the configuration and status pages need a" +" session. Accounts live in ``.Flow_Web/users.json``; the first run seeds" +" the ``admin``/``admin`` administrator, and the frontend warns on every " +"login until those default credentials are changed." +msgstr "" + +#: 5fbb587ae4814cd683d5940abf4af37b +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:1 of +msgid "Account store backing the server web login (``.Flow_Web/users.json``)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore:3 +#: f16349131c524eba9d2fd07591592afb of +msgid "" +"Passwords are PBKDF2-SHA256 records with a per-user salt. A missing " +"store file seeds the default ``admin``/``admin`` administrator; a store " +"file that exists but cannot be read is *not* re-seeded, so a damaged file" +" can never silently restore the default account." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.authenticate:1 +#: cfd5d429421942b3b0576566b76575a8 of +#, python-brace-format +msgid "Return ``{\"username\", \"role\"}`` for valid credentials, else ``None``." +msgstr "" + +#: 56e3012561ed4ed4aa77ffea3a744f93 +#: PyFlow.transfer_web.web_backend.server_backend.UserStore.change_credentials:1 +#: of +msgid "Rename ``username`` and set its password (self-service)." +msgstr "" + +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp:1 +#: ab4c37912c524489a26700248a06704c of +msgid "Flask app + TCP_Server_Base wrapper for the web tool." +msgstr "" + +#: 7e707a76895243cbb7e49d4a943df5f2 +#: PyFlow.transfer_web.web_backend.server_backend.ServerWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_server.json`` and start the TCP server." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po new file mode 100644 index 0000000..d657b4a --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.client_backend.po @@ -0,0 +1,90 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 11:10+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.client_backend.rst:2 +#: a7e084a5059341aa9b7419059c4fa4b0 +msgid "PyFlow.transfer\\_web.web\\_front.client\\_backend module" +msgstr "" + +#: 22485674cb53445a86703fb118698523 +#: PyFlow.transfer_web.web_front.client_backend:1 of +msgid "Flask backend wrapping the PyFlow TCP client for the web tool." +msgstr "" + +#: 31862d77d7184e55a1e729d7e72baed0 +#: PyFlow.transfer_web.web_front.client_backend:3 of +msgid "" +"The launcher (``setup_client.py``) starts this backend and opens the " +"connect UI in the browser. The user enters the server address (an " +"``http``/``https`` domain or a bare IP); the backend queries the server's" +" web backend ``/api/server_info`` for the TCP server address and port, " +"then starts the ``TCP_Client_Base`` instance. The backend stays up to " +"relay the user's frontend actions:" +msgstr "" + +#: 1a028258cda844ee945b9522d51afb9d +#: PyFlow.transfer_web.web_front.client_backend:10 of +msgid "messages/files/folders to the server use the native transfer methods;" +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:11 +#: e160c2a0daf9498cbe2041a8e964ff47 of +msgid "" +"messages to other clients use the native ``/forward_send_msg`` forwarding" +" (a client-only command relayed by the server);" +msgstr "" + +#: 16da18234f0a499893b711e7b555b42a +#: PyFlow.transfer_web.web_front.client_backend:13 of +msgid "" +"files/folders to other clients are forwarded through the built-in " +"``forward_extension_tcp`` extension." +msgstr "" + +#: 79dee862a03f4d0c9bc9403f8d461465 +#: PyFlow.transfer_web.web_front.client_backend:16 of +msgid "" +"The sidebar instance list is kept fresh by the server's " +"``/web_clients_update`` broadcasts; a reload button re-requests the list " +"via ``/web_sync_clients``." +msgstr "" + +#: PyFlow.transfer_web.web_front.client_backend:20 +#: b952223fdafe4d52b8c34498aef1aeef of +msgid "" +"Inbound events (plain-text messages and files pushed by the server, " +"whether direct sends or client forwards) are captured on the TCP client's" +" receive threads through ``TCP_Client_Base``'s " +"``add_message_listener``/``add_file_listener`` APIs, queued here, and " +"polled by the frontend via ``/api/events``." +msgstr "" + +#: 0913e15917534119b775fb5c545c439d +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp:1 of +msgid "Flask app + TCP_Client_Base wrapper for the web tool." +msgstr "" + +#: 3e9a2f40d40b45869c4750ae3e542502 +#: PyFlow.transfer_web.web_front.client_backend.ClientWebApp.start_from_config:1 +#: of +msgid "Read ``.Flow_Web/setup_client.json`` and start the TCP client." +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po new file mode 100644 index 0000000..386ecd0 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/PyFlow.transfer_web.web_front.po @@ -0,0 +1,31 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/PyFlow.transfer_web.web_front.rst:2 +#: 24ab9a394e604e0ea77050304ef77edd +msgid "PyFlow.transfer\\_web.web\\_front package" +msgstr "" + +#: ../../api/PyFlow.transfer_web.web_front.rst:10 +#: ae412dcb5fd34608b462f44c2a8b9a17 +msgid "Submodules" +msgstr "" + diff --git a/docs/locale/zh_TW/LC_MESSAGES/api/index.po b/docs/locale/zh_TW/LC_MESSAGES/api/index.po new file mode 100644 index 0000000..9a4c415 --- /dev/null +++ b/docs/locale/zh_TW/LC_MESSAGES/api/index.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026, RayXu +# This file is distributed under the same license as the PyFlow package. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PyFlow \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-16 10:49+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: zh_TW\n" +"Language-Team: zh_TW \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: ../../api/index.rst:2 8d57b43b04b14ab8a07c05fb89785684 +msgid "API Reference" +msgstr "" + +#: ../../api/index.rst:4 2e33b7c5195747ebb1a15eb5e3e9c026 +msgid "" +"The pages below are generated from the code by ``sphinx-apidoc`` (see the" +" first line of ``docs/reBuild.sh``): each one pulls its text from the " +"docstrings at build time, so nothing here is written by hand." +msgstr "" + diff --git a/docs/reBuild.sh b/docs/reBuild.sh index 0da292c..9d2ee90 100755 --- a/docs/reBuild.sh +++ b/docs/reBuild.sh @@ -1,10 +1,10 @@ +sphinx-apidoc -o api -T -e --separate --module-first --force ../PyFlow sphinx-build -b gettext . _build/gettext sphinx-intl update -p _build/gettext -python3.14t batch_translate_po.py +python3.14t batch_translate_po.py --proxy http://127.0.0.1:7897 ${TRANSLATE_ARGS:-} sphinx-intl build sphinx-build -b html . _build/html/ja -D language=ja sphinx-build -b html . _build/html/ru -D language=ru sphinx-build -b html . _build/html/zh_TW -D language=zh_TW sphinx-build -b html . _build/html/zh_CN -D language=zh_CN sphinx-build -b html . _build/html/ko -D language=ko - diff --git a/docs/templates/adr.md b/docs/templates/adr.md new file mode 100644 index 0000000..16f1817 --- /dev/null +++ b/docs/templates/adr.md @@ -0,0 +1,18 @@ +# Design note: + +> Date: | Status: + +## Context + + +## Decision + + +## Rationale + + +## Rejected alternatives + + +## Consequences + diff --git a/docs/templates/change-note.md b/docs/templates/change-note.md new file mode 100644 index 0000000..d7cd360 --- /dev/null +++ b/docs/templates/change-note.md @@ -0,0 +1,15 @@ +# Change note: PR # + +> One auto-generated note per PR, stored in `docs/changes/`. + +## Core logic of this change + + +## Modules and interactions + + +## Implicit assumptions and easy-to-forget details + + +## Changing this in six months + diff --git a/docs/templates/changelog-entry.md b/docs/templates/changelog-entry.md new file mode 100644 index 0000000..67730c1 --- /dev/null +++ b/docs/templates/changelog-entry.md @@ -0,0 +1,13 @@ +### : + +> Type is one of Added / Changed / Deprecated / Removed / Fixed / Security. + +#### User-facing note + + +#### Concrete changes +- +- + +#### Migration + diff --git a/docs/templates/how-to.md b/docs/templates/how-to.md new file mode 100644 index 0000000..f58a16a --- /dev/null +++ b/docs/templates/how-to.md @@ -0,0 +1,24 @@ +# How-to: + +## Use Case + + +## Prerequisites +- +- + +## Steps +1. + ```bash + + ``` +2. + +## Verification + +```bash + +``` + +## FAQ +- **Q: ** — A: diff --git a/pyproject.toml b/pyproject.toml index 370415b..8572bd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,25 @@ line-length = 100 exclude = ["build/", "dist/", ".git/", "__pycache__/"] [tool.ruff.lint] -select = ["E", "F", "PL", "I"] +select = ["E", "F", "PL", "I", "D"] +# Docstring rules follow docs/DOCSTRING_GUIDE.md (Google style): the NumPy section rules and +# the two mutually exclusive pairs are dropped, everything else stays on. +ignore = [ + "D203", # blank line before a class docstring; conflicts with D211 (Google has none) + "D213", # summary on the second line; conflicts with D212 (Google puts it on the first) + "D406", # NumPy section formatting rules + "D407", + "D408", + "D409", + "D410", + "D411", + "D413", + "D414", +] + +[tool.ruff.lint.per-file-ignores] +# Tests are not public API: the guide lets internal callables go undocumented. +"test/**" = ["D1"] [tool.ruff.format] quote-style = "double" @@ -66,3 +84,12 @@ exclude = ["test/"] [tool.pytest.ini_options] testpaths = ["test"] + +[tool.interrogate] +# Coverage ratchet for the public API. Raise fail-under as docstrings get written; never lower it. +fail-under = 65.0 +style = "google" +ignore-private = true # guide section 5: underscore-prefixed helpers may stay undocumented +ignore-semiprivate = true +exclude = ["test", "docs", "build", "dist", ".venv"] +verbose = 0 diff --git a/test/integration/network_api/test_log_levels.py b/test/integration/network_api/test_log_levels.py new file mode 100644 index 0000000..3c84240 --- /dev/null +++ b/test/integration/network_api/test_log_levels.py @@ -0,0 +1,181 @@ +"""Integration tests for the instance logging levels. + +``is_print_log`` / ``is_debug`` drive how much the server and the client print: +with ``is_print_log=False`` an instance prints nothing, with ``is_print_log=True`` +and ``is_debug=False`` it logs command content and execution results only, and +``is_debug=True`` adds the execution-process lines. The port range is announced +to the connection that just joined, never to every client. +""" + +import contextlib +import io +import socket +import threading +import time + +import pytest +from helpers import wait_until + +from PyFlow.network_api.connect_tcp import TCP_Client_Base, TCP_Server_Base + +_PORT_COUNTER = 64400 +PROCESS_LINES = ( + "new connection:", + "connection count mount:", + "client disconnected:", + "max clients mount:", +) +RESULT_LINES = ( + "TCP server deployed on", + "msg send: hello world", + "server time:", +) +CLIENT_ONLY_LINES = ("connecting to", "connect success!", "[server]") +SESSION_TOKENS = PROCESS_LINES + RESULT_LINES + CLIENT_ONLY_LINES + ("hello world",) + + +def _next_port(): + global _PORT_COUNTER + _PORT_COUNTER += 1 + return _PORT_COUNTER + + +def _run_session(port, server_log=True, server_debug=False, client_log=True, client_debug=False): + """Run one server/client session and return everything the two printed.""" + out = io.StringIO() + with contextlib.redirect_stdout(out): + server = TCP_Server_Base( + host="127.0.0.1", + port=port, + is_extend_command=True, + is_input_command_in_console=False, + is_enable_encrypto=False, + is_debug=server_debug, + is_print_log=server_log, + ) + threading.Thread(target=server.start_TCP_Server, daemon=True).start() + assert wait_until(lambda: server.running), "server did not start" + client = TCP_Client_Base( + host="127.0.0.1", + port=port, + client_host="127.0.0.1", + is_extend_command=True, + is_input_command_in_console=False, + is_enable_encrypto=False, + is_debug=client_debug, + is_print_log=client_log, + ) + try: + assert client.connect() + assert wait_until(lambda: len(server.clients) == 1), "client was not registered" + client.send_message(client.client_socket, "hello world") + assert wait_until(lambda: len(server.messages_dict) == 1), "message was not stored" + client.send_message(client.client_socket, "/time") + assert wait_until(lambda: bool(server.events_dict)), "command was not stored" + if client_log: # the reply is echoed by the client, not by the server + wait_until(lambda: "server time:" in out.getvalue(), timeout=5) + finally: + client.close() + assert wait_until(lambda: not server.clients) + server.stop() # a blocked accept() is not woken by closing the socket + return out.getvalue() + + +def _raw_connect(port, lines=3, timeout=10.0): + """Connect a raw socket and return ``(socket, received_bytes)``.""" + deadline = time.monotonic() + timeout + while True: + try: + sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) + break + except OSError: + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + sock.settimeout(0.2) + buf = b"" + deadline = time.monotonic() + timeout + while buf.count(b"\n") < lines and time.monotonic() < deadline: + try: + chunk = sock.recv(4096) + except TimeoutError: + continue + if not chunk: + break + buf += chunk + return sock, buf + + +def test_print_log_false_prints_nothing(): + """``is_print_log=False`` silences the server and the client completely.""" + port = _next_port() + text = _run_session(port, server_log=False, client_log=False) + # every line this session could print carries its port or one of the tokens + # above, so a stray thread of another test cannot pass for this session + assert str(port) not in text + for token in SESSION_TOKENS: + assert token not in text, token + + +def test_default_logs_command_and_result_only(): + """``is_debug=False`` keeps the command/result lines and drops the process ones.""" + text = _run_session(_next_port()) + for token in RESULT_LINES: + assert token in text, token + for token in PROCESS_LINES: + assert token not in text, token + assert "b'" not in text # no raw byte dumps + + +def test_debug_logs_the_execution_process(): + """``is_debug=True`` adds the execution-process lines and the raw dumps.""" + text = _run_session(_next_port(), server_debug=True) + for token in RESULT_LINES + PROCESS_LINES: + assert token in text, token + assert "b'" in text # received bytes are dumped in debug mode + + +def test_log_flags_are_per_instance(): + """A silent client logs nothing even while its server logs everything.""" + text = _run_session(_next_port(), server_log=True, client_log=False) + assert "TCP server deployed on" in text # the server kept logging + assert "hello world" in text # the server logs the line it received + for token in CLIENT_ONLY_LINES: + assert token not in text, token + + +@pytest.mark.parametrize("asynic", [False, True], ids=["threads", "asyncio"]) +def test_port_range_announcement_is_per_connection(asynic): + """Every client is told the port range on connect, and nobody else is.""" + port = _next_port() + server = TCP_Server_Base( + host="127.0.0.1", + port=port, + is_extend_command=True, + is_input_command_in_console=False, + is_enable_encrypto=False, + is_print_log=False, + is_asynic_clients_io=asynic, + ) + threading.Thread(target=server.start_TCP_Server, daemon=True).start() + assert wait_until(lambda: server.running), "server did not start" + socks = [] + try: + for _ in range(3): + sock, greeting = _raw_connect(port) + socks.append(sock) + assert b"Welcome!" in greeting + assert b"/crypto_mode 0" in greeting + assert b"/client_alloc_port_range NO_LIMIT" in greeting + + # a new connection must not re-announce the range to the established ones + socks[0].settimeout(1.0) + try: + extra = socks[0].recv(4096) + except TimeoutError: + extra = b"" + assert extra == b"" + finally: + for sock in socks: + sock.close() + server.stop() diff --git a/test/integration/network_api/test_tcp_async_server.py b/test/integration/network_api/test_tcp_async_server.py new file mode 100644 index 0000000..ab05b91 --- /dev/null +++ b/test/integration/network_api/test_tcp_async_server.py @@ -0,0 +1,327 @@ +"""Integration tests for the asynic clients io mode of ``TCP_Server_Base``. + +With ``is_asynic_clients_io=True`` the server serves every connection from one +asyncio event loop instead of one thread per client, and ``max_clients`` no +longer caps the connection count. +""" + +import contextlib +import io +import os +import socket +import threading +import time + +import pytest +from helpers import wait_until + +from PyFlow.network_api import rsa_crypto +from PyFlow.network_api.connect_tcp import TCP_Client_Base, TCP_Server_Base + +try: + rsa_crypto.load_library() + HAVE_LIB = True +except rsa_crypto.CryptoLibraryError: + HAVE_LIB = False + +_PORT_COUNTER = 64100 # below the ephemeral range and clear of the other test files' bases + + +def _next_port(): + global _PORT_COUNTER + _PORT_COUNTER += 1 + return _PORT_COUNTER + + +def _redirect_crypto(crypto, tmp_path, ssh_dir, subdir="pub_key"): + """Point a crypto instance's key directories at a temporary location.""" + crypto.pvt_key_dir = str(tmp_path / "pvt_key") + crypto.pub_key_dir = str(tmp_path / subdir) + crypto.ssh_dir = str(ssh_dir) + crypto.registry_path = os.path.join(crypto.pub_key_dir, "pub_key.json") + os.makedirs(crypto.pvt_key_dir, exist_ok=True) + os.makedirs(crypto.pub_key_dir, exist_ok=True) + + +def _read_lines(sock, lines, timeout=10.0): + """Read until ``lines`` newline-terminated lines arrived.""" + sock.settimeout(0.2) + buf = b"" + deadline = time.monotonic() + timeout + while buf.count(b"\n") < lines and time.monotonic() < deadline: + try: + chunk = sock.recv(4096) + except TimeoutError: + continue + if not chunk: + break + buf += chunk + return buf + + +def _read_until(sock, needle, timeout=10.0): + """Read until ``needle`` is seen in the received bytes.""" + sock.settimeout(0.2) + buf = b"" + deadline = time.monotonic() + timeout + while needle not in buf and time.monotonic() < deadline: + try: + chunk = sock.recv(4096) + except TimeoutError: + continue + if not chunk: + break + buf += chunk + return buf + + +def _raw_connect(port, greeting_lines=2, timeout=10.0): + """Connect a raw socket to the server and return ``(socket, greeting)``.""" + deadline = time.monotonic() + timeout + while True: + try: + sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) + break + except OSError: + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + return sock, _read_lines(sock, greeting_lines) + + +def _start_server(**kwargs): + """Start a server with the shared test settings and return it with its thread.""" + kwargs.setdefault("max_clients", 10) + kwargs.setdefault("is_asynic_clients_io", False) + kwargs.setdefault("is_enable_encrypto", False) + server = TCP_Server_Base( + host="127.0.0.1", + port=_next_port(), + is_extend_command=True, + is_input_command_in_console=False, + **kwargs, + ) + thread = threading.Thread(target=server.start_TCP_Server, daemon=True) + thread.start() + return server, thread + + +def _stop_server(server, thread, join=True): + """Stop the server; in asyncio mode also assert its accept loop returned.""" + server.stop() + if join: # thread mode: a blocked accept() is not woken by closing the socket + thread.join(timeout=10) + assert not thread.is_alive(), "start_TCP_Server did not return after stop" + + +@pytest.fixture +def asynic_server(): + """Provide a running server that serves clients from an asyncio event loop.""" + server, thread = _start_server(is_asynic_clients_io=True, max_clients=1) + try: + yield server + finally: + _stop_server(server, thread) + + +@pytest.fixture +def threaded_server(): + """Provide a running server that serves each client in its own thread.""" + server, _thread = _start_server(max_clients=1) + try: + yield server + finally: + server.stop() # a blocked accept() is not woken by closing the socket + + +def test_serves_more_clients_than_max_clients(asynic_server): + """``max_clients`` is ignored: every connection gets a greeting.""" + assert asynic_server.is_asynic_clients_io is True + assert asynic_server.max_clients == 1 # noqa: PLR2004 + socks = [] + try: + for _ in range(8): + sock, greeting = _raw_connect(asynic_server.port) + socks.append(sock) + assert b"Welcome!" in greeting + assert b"/crypto_mode 0" in greeting + assert wait_until(lambda: len(asynic_server.clients) == len(socks)), asynic_server.clients + finally: + for sock in socks: + sock.close() + + +def test_message_and_command_are_served(asynic_server): + """A plain line is acknowledged and stored; a command gets its response.""" + sock, _ = _raw_connect(asynic_server.port) + try: + sock.sendall(b"hello from a coroutine client\n") + assert b"msg send: hello from a coroutine client" in _read_until( + sock, b"msg send: hello" + ) + stored = [entry[0] for entries in asynic_server.messages_dict.values() for entry in entries] + assert stored == ["hello from a coroutine client"] + + sock.sendall(b"/time\n") + assert b"server time:" in _read_until(sock, b"server time:") + + sock.sendall(b"/clients\n") + assert b"online clients (1)" in _read_until(sock, b"online clients") + finally: + sock.close() + + +def test_broadcast_reaches_every_client(asynic_server): + """One push from the server reaches all coroutine clients.""" + socks = [] + try: + for _ in range(3): + sock, _ = _raw_connect(asynic_server.port) + socks.append(sock) + assert wait_until(lambda: len(asynic_server.clients) == 3) # noqa: PLR2004 + + asynic_server.broadcast("broadcast from server") + for sock in socks: + assert b"broadcast from server" in _read_until(sock, b"broadcast from server") + finally: + for sock in socks: + sock.close() + + +def test_disconnected_client_is_dropped(asynic_server): + """A peer that closes frees its slot; the other coroutines keep serving.""" + dead, _ = _raw_connect(asynic_server.port) + alive, _ = _raw_connect(asynic_server.port) + try: + assert wait_until(lambda: len(asynic_server.clients) == 2) # noqa: PLR2004 + dead.close() + assert wait_until(lambda: len(asynic_server.clients) == 1) + + alive.sendall(b"still here\n") + assert b"msg send: still here" in _read_until(alive, b"msg send: still here") + finally: + alive.close() + + +@pytest.mark.parametrize("asynic", [False, True], ids=["threads", "asyncio"]) +def test_file_transfer_over_coroutine_control_channel(asynic, tmp_path): + """A file pushed on a transfer socket arrives while the client is served by the server.""" + server, thread = _start_server(is_asynic_clients_io=asynic) + recv_dir = tmp_path / "server_recv" # never the directory holding the source + recv_dir.mkdir() + server.file_transfer_dir = str(recv_dir) + payload = os.urandom(8192) + src = tmp_path / "upload.bin" + src.write_bytes(payload) + client = TCP_Client_Base( + host="127.0.0.1", + port=server.port, + client_host="127.0.0.1", + is_extend_command=True, + is_input_command_in_console=False, + is_enable_encrypto=False, + ) + try: + assert client.connect() + assert wait_until(lambda: len(server.clients) == 1) + server_sock = server.clients[client.client_socket.getsockname()]["socket"] + server.file_transfer_server_recv_server_start_thread("cid", server_sock, f"/file {src} 0") + port = _wait_transfer_port(client, timeout=30) + assert port is not None, "client did not advertise a transfer port" + + sent = client.file_transfer_mode(str(src), "127.0.0.1", port, 0) + + # the file appears before it is fully written: wait for the complete payload + def received_matches(): + files = [path for path in recv_dir.iterdir() if path.is_file()] + return bool(files) and files[0].read_bytes() == payload + + assert wait_until(received_matches, timeout=30), f"file was not received (client ok={sent})" + finally: + client.close() + _stop_server(server, thread, join=asynic) + + +def _wait_transfer_port(instance, timeout=10.0): + """Wait until an instance advertised a file transfer port.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + with instance.file_transfer_server_port_lock: + ports = list(instance.file_server_port_list) + if ports: + return ports[0][0] + time.sleep(0.05) + return None + + +def test_stop_ends_the_event_loop(asynic_server): + """`stop` releases the accept loop, so the listener stops accepting.""" + port = asynic_server.port + asynic_server.stop() + assert wait_until(lambda: not asynic_server.running) + with pytest.raises(OSError): + socket.create_connection(("127.0.0.1", port), timeout=2) + + +def test_thread_mode_still_refuses_clients_beyond_max_clients(threaded_server): + """The default mode keeps its ``max_clients`` limit and refusal message.""" + port = threaded_server.port + first, greeting = _raw_connect(port) + local = first.getsockname() + try: + # the greeting names the peer: this connection is the one the server registered + assert f"{local[0]}:{local[1]}".encode() in greeting, greeting + assert wait_until(lambda: local in threaded_server.clients), threaded_server.clients + second, refusal = _raw_connect(port, greeting_lines=1) + try: + assert b"Max connection mount" in refusal + assert len(threaded_server.clients) == 1 + finally: + second.close() + finally: + first.close() + + +@pytest.mark.skipif( + not HAVE_LIB, + reason="libcrypto_api not built (run cmake -S . -B build && cmake --build build first)", +) +def test_encrypted_channel_round_trip(tmp_path): + """A coroutine client completes the RSA handshake and exchanges messages.""" + ssh_dir = tmp_path / "ssh" + ssh_dir.mkdir() + server, thread = _start_server( + is_asynic_clients_io=True, + max_clients=1, + is_enable_encrypto=True, + ) + _redirect_crypto(server.crypto, tmp_path, ssh_dir, "pub_key") + client = TCP_Client_Base( + host="127.0.0.1", + port=server.port, + client_host="127.0.0.1", + is_extend_command=True, + is_input_command_in_console=False, + is_enable_encrypto=True, + ) + _redirect_crypto(client.crypto, tmp_path, ssh_dir, "pub_key_client") + try: + assert client.connect() + assert wait_until(lambda: client.client_socket in client._encrypted_sockets, timeout=20) + assert wait_until(lambda: len(server._encrypted_sockets) == 1, timeout=20) + + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + client.send_message(client.client_socket, "encrypted hello") + assert wait_until( + lambda: sum(len(v) for v in server.messages_dict.values()) == 1, + timeout=10, + ) + # the ack is echoed by the client's receive thread: wait for it + assert wait_until(lambda: "msg send: encrypted hello" in buf.getvalue(), timeout=10) + stored = [entry[0] for entries in server.messages_dict.values() for entry in entries] + assert stored == ["encrypted hello"] + assert "msg send: encrypted hello" in buf.getvalue() + finally: + client.close() + _stop_server(server, thread) diff --git a/test/integration/test_command_handlers.py b/test/integration/test_command_handlers.py index 4d0c4d0..0c06987 100644 --- a/test/integration/test_command_handlers.py +++ b/test/integration/test_command_handlers.py @@ -26,6 +26,12 @@ def __init__(self): def sendall(self, b): self.data += b + def shutdown(self, how): + pass + + def close(self): + pass + client_addr = ("127.0.0.1", 12345) dummy = DummySocket() diff --git a/test/integration/test_forward_extension.py b/test/integration/test_forward_extension.py index 03046b9..aa0b0b4 100644 --- a/test/integration/test_forward_extension.py +++ b/test/integration/test_forward_extension.py @@ -26,6 +26,9 @@ def __init__(self): def sendall(self, data): self.data += data + def shutdown(self, how): + pass + def close(self): pass diff --git a/test/unit/transfer_web/test_account_flow.py b/test/unit/transfer_web/test_account_flow.py new file mode 100644 index 0000000..de87ec3 --- /dev/null +++ b/test/unit/transfer_web/test_account_flow.py @@ -0,0 +1,524 @@ +"""Registration, password reset, client login and contacts over the web API. + +The client-facing half of the server: accounts are registered with a mailed code, +passwords are reset the same way, web clients log in with a password or a code and +receive a session token, and the instance list a client receives holds only the +accounts that accepted it as a contact. +""" + +import threading + +import pytest + +from PyFlow.transfer_web.web_backend import server_backend + + +class FakeTcpServer: + """Stand-in for ``TCP_Server_Base`` that records what the web layer pushes.""" + + def __init__(self): + self.running = True + self.host = "127.0.0.1" + self.port = 65432 + self.is_enable_encrypto = False + self.clients = {} + self.client_lock = threading.RLock() + self.file_transfer_dir = "/tmp" + self.pushes = [] # (socket, text) + + def send_message(self, socket, message): + self.pushes.append((socket, message)) + return True + + +@pytest.fixture +def web(tmp_path, monkeypatch): + """A ServerWebApp with a fake TCP server and a recording mailbox.""" + monkeypatch.setattr(server_backend, "FLOW_WEB_DIR", str(tmp_path)) + monkeypatch.setattr(server_backend, "SECRET_KEY_FILE", str(tmp_path / "web_secret_key")) + app = server_backend.ServerWebApp( + db_path=str(tmp_path / "flow_web.db"), + mail_config_path=str(tmp_path / "email_config.json"), + ) + app.app.config.update(TESTING=True) + app.server = FakeTcpServer() + app.sent = [] + + def record(to_address, code, purpose, expires_in): + app.sent.append( + {"to": to_address, "code": code, "purpose": purpose, "expires_in": expires_in} + ) + + monkeypatch.setattr(app.mail, "send_code", record) + yield app + app.users.close() + + +@pytest.fixture +def client(web): + return web.app.test_client() + + +def code_for(web, purpose, address): + """Return the last code the fake mailbox delivered.""" + matching = [c for c in web.sent if c["purpose"] == purpose and c["to"] == address] + assert matching, f"no {purpose} code was sent to {address}" + return matching[-1]["code"] + + +def register(client, web, username, email): + """Run the full registration flow and return the new account.""" + assert client.post("/api/register/send_code", json={"email": email}).status_code == 200 + resp = client.post( + "/api/register", + json={ + "username": username, + "email": email, + "password": f"{username}s-password", + "code": code_for(web, "register", email), + }, + ) + assert resp.status_code == 200, resp.get_json() + return resp.get_json()["user"] + + +def client_login(client, identify, password=None, code=None): + return client.post( + "/api/client_login", json={"identify": identify, "password": password, "code": code} + ) + + +def login_token(client, web, identify, password): + """Run the full two-factor client login and return the session token.""" + assert client.post("/api/login/send_code", json={"identify": identify}).status_code == 200 + account = web.users.find(identify) + code = code_for(web, "login", account["email"]) + data = client_login(client, identify, password=password, code=code).get_json() + assert data["ok"], data + return data["token"] + + +def bind(web, address, token): + """Attach a fake TCP connection to the account owning ``token``.""" + socket = object() + web.server.clients[address] = { + "socket": socket, + "address": address, + "id": f"{address[0]}:{address[1]}", + } + web._on_web_bind(socket, address, f"/web_bind {token}") + return socket + + +def test_the_public_server_info_and_status_survive_a_running_tcp_server(client, web): + assert client.get("/api/server_info").get_json() == { + "host": "127.0.0.1", + "port": 65432, + "is_enable_encrypto": False, + } + login = client.post("/api/login", json={"identify": "admin", "password": "admin"}) + assert login.status_code == 200 + status = client.get("/api/status").get_json() + assert status["running"] is True + assert status["server_info"]["port"] == 65432 + assert status["clients"] == [] + + +def test_registration_mails_a_code_and_creates_the_account(client, web): + first = client.post("/api/register/send_code", json={"email": "carol@example.com"}) + assert first.status_code == 200 + issued = client.post("/api/register/send_code", json={"email": "carol@example.com"}) + assert issued.status_code == 400 # one code per minute + assert "wait" in issued.get_json()["error"] + + code = code_for(web, "register", "carol@example.com") + assert code.isdigit() and len(code) == 6 + + resp = client.post( + "/api/register", + json={ + "username": "carol", + "email": "carol@example.com", + "password": "carols-password", + "code": "000000" if code != "000000" else "111111", + }, + ) + assert resp.status_code == 400 + assert "incorrect" in resp.get_json()["error"] + + resp = client.post( + "/api/register", + json={ + "username": "carol", + "email": "carol@example.com", + "password": "carols-password", + "code": code, + }, + ) + assert resp.status_code == 200, resp.get_json() + user = resp.get_json()["user"] + assert user["username"] == "carol" + assert user["email"] == "carol@example.com" + assert user["role"] == "user" + assert len(user["user_id"]) == 8 + assert "password" not in user + + # the console login accepts the new account, by name and by email + assert client.post( + "/api/login", json={"identify": "carol", "password": "carols-password"} + ).status_code == 200 + + +def test_registration_rejects_a_taken_or_malformed_email(client, web): + register(client, web, "carol", "carol@example.com") + resp = client.post("/api/register/send_code", json={"email": "CAROL@example.com"}) + assert resp.status_code == 400 + assert "already registered" in resp.get_json()["error"] + assert client.post("/api/register/send_code", json={"email": "nope"}).status_code == 400 + + +def test_registration_needs_a_mailbox_that_works(tmp_path, monkeypatch): + monkeypatch.setattr(server_backend, "SECRET_KEY_FILE", str(tmp_path / "key")) + app = server_backend.ServerWebApp( + db_path=str(tmp_path / "flow_web.db"), mail_config_path=str(tmp_path / "mail.json") + ) + app.app.config.update(TESTING=True) + resp = app.app.test_client().post("/api/register/send_code", json={"email": "a@example.com"}) + assert resp.status_code == 400 + assert "not configured" in resp.get_json()["error"] + app.users.close() + + +def test_password_reset_verifies_the_code_before_storing_the_new_password(client, web): + user = register(client, web, "carol", "carol@example.com") + + resp = client.post("/api/password/send_code", json={"identify": user["user_id"]}) + assert resp.status_code == 200 + assert resp.get_json()["masked_email"] == "c***@example.com" + assert resp.get_json()["expires_in"] == 300 + code = code_for(web, "reset_password", "carol@example.com") + + resp = client.post( + "/api/password/reset", + json={"identify": "carol", "code": "000000" if code != "000000" else "111111", + "password": "brand-new-password"}, + ) + assert resp.status_code == 400 + assert client.post( + "/api/login", json={"identify": "carol", "password": "carols-password"} + ).status_code == 200 # the rejected reset left the old password alone + + resp = client.post( + "/api/password/reset", + json={"identify": "carol", "code": code, "password": "brand-new-password"}, + ) + assert resp.status_code == 200 + assert client.post( + "/api/login", json={"identify": "carol", "password": "carols-password"} + ).status_code == 401 + assert client.post( + "/api/login", json={"identify": "carol", "password": "brand-new-password"} + ).status_code == 200 + + +def test_password_reset_rejects_unknown_accounts_and_accounts_without_email(client, web): + resp = client.post("/api/password/send_code", json={"identify": "nobody"}) + assert resp.status_code == 400 + assert "no account matches" in resp.get_json()["error"] + + resp = client.post("/api/password/send_code", json={"identify": "admin"}) # seeded, no email + assert resp.status_code == 400 + assert "no email address" in resp.get_json()["error"] + + +def test_client_login_needs_the_password_and_a_mailed_code(client, web): + user = register(client, web, "carol", "carol@example.com") + assert client.post("/api/login/send_code", json={"identify": "carol"}).status_code == 200 + code = code_for(web, "login", "carol@example.com") + + resp = client_login(client, "carol@example.com", password="carols-password", code=code) + assert resp.status_code == 200, resp.get_json() + data = resp.get_json() + assert data["user"]["user_id"] == user["user_id"] + assert data["user"]["username"] == "carol" + token = data["token"] + + assert client.post("/api/client_verify", json={"token": token}).get_json()["ok"] is True + assert client.post("/api/client_verify", json={"token": "bogus"}).status_code == 401 + assert client.post("/api/client_logout", json={"token": token}).status_code == 200 + assert client.post("/api/client_verify", json={"token": token}).status_code == 401 + + +def test_client_login_rejects_a_single_factor(client, web): + register(client, web, "carol", "carol@example.com") + assert client.post("/api/login/send_code", json={"identify": "carol"}).status_code == 200 + code = code_for(web, "login", "carol@example.com") + + only_code = client_login(client, "carol@example.com", code=code) + assert only_code.status_code == 400 + assert "password and the mailed verification code" in only_code.get_json()["error"] + + only_password = client_login(client, "carol@example.com", password="carols-password") + assert only_password.status_code == 400 + assert "password and the mailed verification code" in only_password.get_json()["error"] + + # neither attempt spent the code, so the full pair still works afterwards + assert client_login( + client, "carol@example.com", password="carols-password", code=code + ).status_code == 200 + + +def test_client_login_rejects_a_wrong_password_or_code(client, web): + register(client, web, "carol", "carol@example.com") + assert client.post("/api/login/send_code", json={"identify": "carol"}).status_code == 200 + code = code_for(web, "login", "carol@example.com") + + wrong_password = client_login( + client, "carol@example.com", password="not-the-password", code=code + ) + assert wrong_password.status_code == 401 + assert "invalid account or password" in wrong_password.get_json()["error"] + + wrong_code = "000000" if code != "000000" else "111111" + assert client_login( + client, "carol@example.com", password="carols-password", code=wrong_code + ).status_code == 401 + + # the code is spent by the successful login only + assert client_login( + client, "carol@example.com", password="carols-password", code=code + ).status_code == 200 + assert client_login( + client, "carol@example.com", password="carols-password", code=code + ).status_code == 401 # a code is single use + + +def test_client_login_rejects_unknown_accounts(client, web): + assert client_login(client, "carol").status_code == 400 + assert client_login(client, "", password="carols-password", code="123456").status_code == 400 + assert client.post("/api/login/send_code", json={"identify": "nobody"}).status_code == 400 + + +def test_client_verify_checks_the_saved_credentials_against_the_token(client, web): + user = register(client, web, "carol", "carol@example.com") + assert client.post("/api/login/send_code", json={"identify": "carol"}).status_code == 200 + code = code_for(web, "login", "carol@example.com") + token = client_login( + client, "carol", password="carols-password", code=code + ).get_json()["token"] + + replay = {"token": token, "identify": "carol", "password": "carols-password"} + assert client.post("/api/client_verify", json=replay).get_json()["user"]["user_id"] == user[ + "user_id" + ] + + stale = dict(replay, password="an-old-password") + rejected = client.post("/api/client_verify", json=stale) + assert rejected.status_code == 401 + assert "no longer open this account" in rejected.get_json()["error"] + + half = {"token": token, "identify": "carol"} + assert client.post("/api/client_verify", json=half).status_code == 400 + + # credentials of another account never unlock this session + register(client, web, "dave", "dave@example.com") + other = dict(replay, identify="dave", password="daves-password") + assert client.post("/api/client_verify", json=other).status_code == 401 + + +def test_a_client_sees_no_instance_until_the_contact_is_accepted(web): + admin = web.app.test_client() + register(admin, web, "alice", "alice@example.com") + register(admin, web, "bob", "bob@example.com") + + alice_addr, bob_addr = ("127.0.0.1", 1111), ("127.0.0.1", 2222) + alice_token = login_token(admin, web, "alice", "alices-password") + bob_token = login_token(admin, web, "bob", "bobs-password") + bind(web, alice_addr, alice_token) + bind(web, bob_addr, bob_token) + + # bound but not contacts: neither sees the other + assert web._client_list_for(alice_addr) == [] + assert web._client_list_for(bob_addr) == [] + + search = admin.post("/api/contacts/search", json={"token": alice_token, "query": "bob"}) + assert search.status_code == 200 + found = search.get_json()["results"][0] + assert found["username"] == "bob" + assert found["online"] is True + assert found["relation"] == "none" + + assert admin.post( + "/api/contacts/request", json={"token": alice_token, "user_id": found["user_id"]} + ).status_code == 200 + assert admin.post( + "/api/contacts/search", json={"token": alice_token, "query": "bob"} + ).get_json()["results"][0]["relation"] == "outgoing" + + requests = admin.get(f"/api/contact_requests?token={bob_token}").get_json() + assert [r["user"]["username"] for r in requests["incoming"]] == ["alice"] + assert admin.get(f"/api/contact_requests?token={alice_token}").get_json()["outgoing"] + + resp = admin.post( + "/api/contacts/respond", + json={"token": bob_token, "request_id": requests["incoming"][0]["id"], "accept": True}, + ) + assert resp.status_code == 200 + assert resp.get_json()["user"]["username"] == "alice" + + # both directions now see each other, with the account name attached + alice_view = web._client_list_for(alice_addr) + assert [(c["ip"], c["port"], c["username"]) for c in alice_view] == [ + ("127.0.0.1", 2222, "bob") + ] + bob_view = web._client_list_for(bob_addr) + assert [(c["ip"], c["port"], c["username"]) for c in bob_view] == [("127.0.0.1", 1111, "alice")] + assert admin.post( + "/api/contacts/search", json={"token": alice_token, "query": "bob"} + ).get_json()["results"][0]["relation"] == "contact" + + +def test_unbound_or_logged_out_clients_see_nothing(web): + admin = web.app.test_client() + alice = register(admin, web, "alice", "alice@example.com") + bob = register(admin, web, "bob", "bob@example.com") + alice_addr, bob_addr = ("127.0.0.1", 1111), ("127.0.0.1", 2222) + alice_token = login_token(admin, web, "alice", "alices-password") + bob_token = login_token(admin, web, "bob", "bobs-password") + bind(web, alice_addr, alice_token) + bind(web, bob_addr, bob_token) + + # bob has nothing: alice (not a contact yet) has not asked him + assert admin.get(f"/api/contact_requests?token={bob_token}").get_json()["incoming"] == [] + admin.post("/api/contacts/request", json={"token": alice_token, "user_id": bob["user_id"]}) + requests = admin.get(f"/api/contact_requests?token={bob_token}").get_json() + request_id = requests["incoming"][0]["id"] + admin.post( + "/api/contacts/respond", json={"token": bob_token, "request_id": request_id, "accept": True} + ) + assert web._client_list_for(alice_addr) + assert web._client_list_for(("127.0.0.1", 3333)) == [] # no /web_bind, no contacts + + # logging the account out unbinds its connection and drops its requests + admin.post("/api/client_logout", json={"token": alice_token}) + assert web._client_list_for(alice_addr) == [] + assert alice["user_id"] != bob["user_id"] + + +def test_bind_acknowledges_the_address_and_pushes_the_list(web): + admin = web.app.test_client() + register(admin, web, "alice", "alice@example.com") + token = login_token(admin, web, "alice", "alices-password") + address = ("127.0.0.1", 4444) + socket = bind(web, address, token) + + acks = [ + text + for sock, text in web.server.pushes + if sock is socket and text.startswith("/web_bind_ok") + ] + assert len(acks) == 1 + assert '"port": 4444' in acks[0] + pushes = [text for sock, text in web.server.pushes if text.startswith("/web_clients_update")] + assert pushes and pushes[-1] == "/web_clients_update []" + + # an unknown token is refused and binds nothing + bind(web, ("127.0.0.1", 5555), "not-a-token") + assert web._client_list_for(("127.0.0.1", 5555)) == [] + + +def test_contacts_endpoints_require_a_valid_token(web): + anonymous = web.app.test_client() + assert anonymous.get("/api/contact_requests").status_code == 401 + assert anonymous.post("/api/contacts/search", json={"query": "x"}).status_code == 401 + assert anonymous.post("/api/contacts/request", json={"user_id": "x"}).status_code == 401 + assert anonymous.post( + "/api/contacts/respond", json={"request_id": 1, "accept": True} + ).status_code == 401 + + register(anonymous, web, "alice", "alice@example.com") + token = login_token(anonymous, web, "alice", "alices-password") + assert anonymous.post( + "/api/contacts/search", json={"token": token, "query": " "} + ).status_code == 400 + + +def test_contact_requests_are_answered_one_way_only(web): + admin = web.app.test_client() + alice = register(admin, web, "alice", "alice@example.com") + bob = register(admin, web, "bob", "bob@example.com") + register(admin, web, "carol", "carol@example.com") + alice_token = login_token(admin, web, "alice", "alices-password") + bob_token = login_token(admin, web, "bob", "bobs-password") + carol_token = login_token(admin, web, "carol", "carols-password") + + admin.post("/api/contacts/request", json={"token": alice_token, "user_id": bob["user_id"]}) + requests = admin.get(f"/api/contact_requests?token={bob_token}").get_json() + request_id = requests["incoming"][0]["id"] + + assert admin.post( + "/api/contacts/respond", + json={"token": carol_token, "request_id": request_id, "accept": True}, + ).status_code == 400 # not addressed to carol + assert admin.post( + "/api/contacts/respond", json={"token": bob_token, "request_id": 4242, "accept": True} + ).status_code == 400 + + assert admin.post( + "/api/contacts/respond", + json={"token": bob_token, "request_id": request_id, "accept": False}, + ).status_code == 200 + assert web.users.are_contacts(alice["user_id"], bob["user_id"]) is False + + # adding an account twice is refused once the two are contacts + admin.post("/api/contacts/request", json={"token": alice_token, "user_id": bob["user_id"]}) + incoming = admin.get(f"/api/contact_requests?token={bob_token}").get_json() + request_id = incoming["incoming"][0]["id"] + admin.post( + "/api/contacts/respond", json={"token": bob_token, "request_id": request_id, "accept": True} + ) + again = admin.post( + "/api/contacts/request", json={"token": alice_token, "user_id": bob["user_id"]} + ) + assert again.status_code == 400 + assert "already a contact" in again.get_json()["error"] + + +def test_email_config_is_validated_and_the_password_is_not_echoed(client, web, monkeypatch): + assert client.post( + "/api/login", json={"identify": "admin", "password": "admin"} + ).status_code == 200 + + config = { + "host": "smtp.example.com", + "port": 465, + "username": "bot@example.com", + "password": "authorization-code", + "from": "PyFlow ", + "encryption": "ssl", + } + refused = client.post("/api/email_config", json={"config": config}) + assert refused.status_code == 400 # no such server behind the host name + assert client.get("/api/email_config").get_json()["enabled"] is False + + monkeypatch.setattr(web.mail, "_connect", lambda settings: FakeSession()) + accepted = client.post("/api/email_config", json={"config": config}) + assert accepted.status_code == 200, accepted.get_json() + assert accepted.get_json()["enabled"] is True + assert "password" not in accepted.get_json()["config"] + + stored = client.get("/api/email_config").get_json() + assert stored["enabled"] is True + assert stored["config"]["host"] == "smtp.example.com" + assert "password" not in stored["config"] + assert web.mail.is_enabled() is True + + +class FakeSession: + """Stand-in for an authenticated SMTP session.""" + + def quit(self): + """Close the session.""" + + def close(self): + """Close the session.""" diff --git a/test/unit/transfer_web/test_client_account.py b/test/unit/transfer_web/test_client_account.py new file mode 100644 index 0000000..c13ae65 --- /dev/null +++ b/test/unit/transfer_web/test_client_account.py @@ -0,0 +1,437 @@ +"""Client login, auto-login and contact proxying of the web client backend. + +The client backend holds the account session and the saved credentials, so these +tests pin the contract the browser relies on: a connected but anonymous client +gets the login window, a successful login writes +``.Flow_Web/client_login.json``, every later page load logs in again from that +file, the session token is bound to the TCP connection, and logging out deletes +the file. The client talks to a real server web backend through its Flask test +client, so the two sides are exercised together without opening sockets. +""" + +import contextlib +import json +import os +import stat +import threading +import time +from urllib.parse import parse_qs + +import flask +import pytest + +from PyFlow.transfer_web.web_backend import server_backend +from PyFlow.transfer_web.web_front import client_backend +from PyFlow.transfer_web.web_front.client_backend import _ServerRequestError + + +class FakeSocket: + """Socket stand-in exposing the bound address of the client.""" + + def getsockname(self): + """Return the local address of the pretend connection.""" + return ("127.0.0.1", 40000) + + +class FakeTcpClient: + """Minimal ``TCP_Client_Base`` stand-in that records written commands.""" + + def __init__(self): + self.running = True + self.is_enable_encrypto = False + self.client_host = "127.0.0.1" + self.client_port = 40000 + self.client_socket = FakeSocket() + self._crypto_lock = threading.Lock() + self._encrypted_sockets = set() + self.sent = [] + + def send_message(self, client_socket, message): + """Record one command written to the connection.""" + self.sent.append(message) + return True + + def close(self): + """Drop the pretend connection.""" + self.running = False + + +@contextlib.contextmanager +def rendered_templates(app): + """Collect the template names Flask renders for one request.""" + names = [] + + def record(sender, template, context, **extra): + names.append(template.name) + + flask.template_rendered.connect(record, app) + try: + yield names + finally: + flask.template_rendered.disconnect(record, app) + + +@pytest.fixture +def server(tmp_path, monkeypatch): + """The server web backend the client backend logs in to.""" + monkeypatch.setattr(server_backend, "SECRET_KEY_FILE", str(tmp_path / "server_key")) + app = server_backend.ServerWebApp( + db_path=str(tmp_path / "server.db"), mail_config_path=str(tmp_path / "mail.json") + ) + app.app.config.update(TESTING=True) + app.sent = [] + + def record_code(to_address, code, purpose, expires_in): + app.sent.append({"to": to_address, "code": code, "purpose": purpose}) + + monkeypatch.setattr(app.mail, "send_code", record_code) + yield app + app.users.close() + + +@pytest.fixture +def account(server): + """Register an account on the server and return its record.""" + client = server.app.test_client() + sent = client.post("/api/register/send_code", json={"email": "alice@example.com"}) + assert sent.status_code == 200 + code = server.sent[-1]["code"] + resp = client.post( + "/api/register", + json={ + "username": "alice", + "email": "alice@example.com", + "password": "alices-password", + "code": code, + }, + ) + assert resp.status_code == 200, resp.get_json() + return resp.get_json()["user"] + + +@pytest.fixture +def web(tmp_path, monkeypatch, server): + """A ClientWebApp connected to the in-process server backend.""" + monkeypatch.setattr(client_backend, "FLOW_WEB_DIR", str(tmp_path)) + monkeypatch.setattr(client_backend, "CLIENT_LOGIN_FILE", str(tmp_path / "client_login.json")) + monkeypatch.setattr( + client_backend, "CLIENT_LAST_SERVER_FILE", str(tmp_path / "client_last_server.json") + ) + monkeypatch.setattr(client_backend, "CLIENT_CONFIG_FILE", str(tmp_path / "setup_client.json")) + monkeypatch.setattr( + client_backend, + "CLIENT_EXTENSIONS_UI_FILE", + str(tmp_path / "client_extensions_ui.json"), + ) + app = client_backend.ClientWebApp(web_port=5099) + app.app.config.update(TESTING=True) + app.connected = True + app.client = FakeTcpClient() + app._server_base = "http://server.test" + + server_client = server.app.test_client() + + def request(path, payload=None): + """Call the server backend the way the real HTTP request would.""" + route, _, query = path.partition("?") + if query: + params = {key: value[0] for key, value in parse_qs(query).items()} + response = server_client.get(route, query_string=params) + elif payload is None: + response = server_client.get(route) + else: + response = server_client.post(route, json=payload) + data = response.get_json() or {} + if response.status_code >= 400 or data.get("ok") is False: + raise _ServerRequestError( + response.status_code, data.get("error") or f"HTTP {response.status_code}" + ) + return data + + monkeypatch.setattr(app, "_server_request", request) + return app + + +@pytest.fixture +def client(web): + return web.app.test_client() + + +def login(client, server, identify="alice", password="alices-password", code=None): + """Log the web client in with both factors, mailing a fresh code first.""" + if code is None: + sent = client.post("/api/login/send_code", json={"identify": identify}) + assert sent.status_code == 200, sent.get_json() + code = [c for c in server.sent if c["purpose"] == "login"][-1]["code"] + return client.post( + "/api/login", json={"identify": identify, "password": password, "code": code} + ) + + +def wait_for_bind(web, timeout=5): + """Wait until the bind loop wrote ``/web_bind`` on the connection.""" + deadline = time.time() + timeout + while time.time() < deadline: + if any(message.startswith("/web_bind ") for message in web.client.sent): + return True + time.sleep(0.05) + return False + + +def test_connected_client_without_a_session_gets_the_login_window(web, client): + with rendered_templates(web.app) as names: + resp = client.get("/") + assert resp.status_code == 200 + assert names == ["client_login.html"] + + +def test_disconnected_client_gets_the_connect_page(web, client): + web.connected = False + with rendered_templates(web.app) as names: + client.get("/") + assert names == ["client_connect.html"] + + +def test_forced_login_saves_the_password_and_the_session_token( + web, client, server, account, tmp_path +): + resp = login(client, server) + assert resp.status_code == 200, resp.get_json() + assert resp.get_json()["user"]["user_id"] == account["user_id"] + + saved = json.loads((tmp_path / "client_login.json").read_text(encoding="utf-8")) + assert saved["server"] == "http://server.test" + assert saved["identify"] == "alice" + assert saved["password"] == "alices-password" + assert saved["token"] == web.session["token"] + assert saved["token"] + if os.name == "posix": + assert stat.S_IMODE(os.stat(tmp_path / "client_login.json").st_mode) == 0o600 + + status = client.get("/api/status").get_json() + assert status["logged_in"] is True + assert status["user"]["username"] == "alice" + assert status["server_address"] == "http://server.test" + + with rendered_templates(web.app) as names: + assert client.get("/").status_code == 200 + assert names == ["client_main.html"] + assert web.client.sent and wait_for_bind(web) + + +def test_forced_login_needs_the_password_and_a_mailed_code(web, client, server, account): + assert client.post("/api/login/send_code", json={"identify": "alice"}).status_code == 200 + code = [c for c in server.sent if c["purpose"] == "login"][-1]["code"] + + only_password = login(client, server, password="alices-password", code="") + assert only_password.status_code == 400 + assert "password and the mailed verification code" in only_password.get_json()["error"] + + only_code = login(client, server, password="", code=code) + assert only_code.status_code == 400 + assert "password and the mailed verification code" in only_code.get_json()["error"] + + assert web.session is None # neither attempt opened a session + assert login(client, server, code=code).status_code == 200 + + +def test_forced_login_reports_a_wrong_password_or_code(web, client, server, account): + assert client.post("/api/login/send_code", json={"identify": "alice"}).status_code == 200 + code = [c for c in server.sent if c["purpose"] == "login"][-1]["code"] + + wrong_password = login(client, server, password="not-the-password", code=code) + assert wrong_password.status_code == 401 + assert "invalid" in wrong_password.get_json()["error"] + + bogus = "000000" if code != "000000" else "111111" + wrong_code = login(client, server, password="alices-password", code=bogus) + assert wrong_code.status_code == 401 + assert "incorrect" in wrong_code.get_json()["error"] + + assert login(client, server, password="alices-password", code=code).status_code == 200 + + +def test_forced_login_needs_an_account(client): + missing = client.post("/api/login", json={"identify": "", "password": "x", "code": "1"}) + assert missing.status_code == 400 + assert client.post( + "/api/login", json={"identify": "alice", "password": "x", "code": "1"} + ).status_code == 401 # unknown account, refused by the server + + +def test_a_later_page_load_logs_in_again_from_the_saved_file(web, client, server, account): + assert login(client, server).status_code == 200 + web.session = None # as after the client backend is restarted + + with rendered_templates(web.app) as names: + assert client.get("/").status_code == 200 + assert names == ["client_main.html"] + assert web.session["user"]["username"] == "alice" + + +def test_saved_credentials_of_another_server_are_ignored(web, client, tmp_path, account): + (tmp_path / "client_login.json").write_text( + json.dumps( + {"server": "http://other.test", "identify": "alice", "password": "alices-password"} + ), + encoding="utf-8", + ) + with rendered_templates(web.app) as names: + client.get("/") + assert names == ["client_login.html"] + assert web.session is None + + +def test_rejected_saved_credentials_are_explained_on_the_login_page( + web, client, server, tmp_path, account +): + assert login(client, server).status_code == 200 + token = web.session["token"] + web.session = None + # the session token is still known, the saved password is not + (tmp_path / "client_login.json").write_text( + json.dumps( + { + "server": "http://server.test", + "identify": "alice", + "password": "stale-password", + "token": token, + } + ), + encoding="utf-8", + ) + body = client.get("/").get_data(as_text=True) + assert "stale-password" not in body + assert "saved credentials were rejected" in body + assert "alice" in body # the account is prefilled for the retry + assert web.session is None + + +def test_a_saved_login_without_a_token_or_password_needs_a_fresh_login( + web, client, server, tmp_path, account +): + (tmp_path / "client_login.json").write_text( + json.dumps( + {"server": "http://server.test", "identify": "alice", "password": "alices-password"} + ), + encoding="utf-8", + ) + body = client.get("/").get_data(as_text=True) + assert "the saved login is incomplete" in body + assert web.session is None + + +def test_the_session_token_is_bound_to_the_tcp_connection(web, client, server, account): + assert login(client, server).status_code == 200 + assert wait_for_bind(web) + assert web.client.sent[0] == f"/web_bind {web.session['token']}" + + web._on_bind_ok(None, None, '/web_bind_ok {"ip": "10.0.0.5", "port": 4321}') + assert web._own_address() == {"ip": "10.0.0.5", "port": 4321, "id": "10.0.0.5:4321"} + + +def test_logout_deletes_the_file_and_ends_the_server_session( + web, client, server, tmp_path, account +): + assert login(client, server).status_code == 200 + token = web.session["token"] + + assert client.post("/api/logout").get_json()["ok"] is True + assert web.session is None + assert not (tmp_path / "client_login.json").exists() + verify = server.app.test_client().post("/api/client_verify", json={"token": token}) + assert verify.status_code == 401 + + +def test_contact_proxies_need_a_session_and_pass_the_token_through( + web, client, server, account +): + assert client.post("/api/contacts/search", json={"query": "alice"}).status_code == 401 + assert client.get("/api/contact_requests").status_code == 401 + + second = account + assert login(client, server, identify="alice", password="alices-password").status_code == 200 + token = web.session["token"] + + # alice finds herself nowhere and cannot add her own account + found = client.post("/api/contacts/search", json={"query": "alice"}).get_json() + assert found["results"] == [] + own = client.post("/api/contacts/request", json={"user_id": second["user_id"]}) + assert own.status_code == 400 + assert "your own account" in own.get_json()["error"] + + requests = client.get("/api/contact_requests").get_json() + assert requests["incoming"] == [] + assert web.session["token"] == token + + +def test_contacts_can_be_added_answered_and_become_mutual(web, client, server, account): + bob = _register(server, "bob", "bob@example.com") + assert login(client, server, "alice", "alices-password").status_code == 200 + + results = client.post("/api/contacts/search", json={"query": bob["user_id"]}) + found = results.get_json()["results"] + assert [entry["relation"] for entry in found] == ["none"] + assert client.post("/api/contacts/request", json={"user_id": bob["user_id"]}).status_code == 200 + assert client.post("/api/contacts/search", json={"query": "bob"}).get_json()["results"][0][ + "relation" + ] == "outgoing" + + bob_client, bob_token = server_login(server, "bob", "bobs-password") + pending = bob_client.get(f"/api/contact_requests?token={bob_token}").get_json()["incoming"] + assert [entry["user"]["username"] for entry in pending] == ["alice"] + assert bob_client.post( + "/api/contacts/respond", + json={"token": bob_token, "request_id": pending[0]["id"], "accept": True}, + ).status_code == 200 + + # the contact is mutual now, and both sides are back in the search results + assert server.users.are_contacts(account["user_id"], bob["user_id"]) is True + assert server.users.are_contacts(bob["user_id"], account["user_id"]) is True + assert client.post("/api/contacts/search", json={"query": "bob"}).get_json()["results"][0][ + "relation" + ] == "contact" + + +def test_a_server_401_drops_the_session_but_keeps_the_credentials( + web, client, server, account, tmp_path +): + assert login(client, server).status_code == 200 + web.session["token"] = "revoked-token" # the server no longer knows this session + + resp = client.get("/api/contact_requests") + assert resp.status_code == 401 + assert "login required" in resp.get_json()["error"] + assert web.session is None + assert (tmp_path / "client_login.json").exists() # the saved credentials survive + + +def server_login(server, identify, password): + """Run the two-factor client login straight against the server backend.""" + client = server.app.test_client() + assert client.post("/api/login/send_code", json={"identify": identify}).status_code == 200 + code = [c for c in server.sent if c["purpose"] == "login"][-1]["code"] + resp = client.post( + "/api/client_login", + json={"identify": identify, "password": password, "code": code}, + ) + assert resp.status_code == 200, resp.get_json() + return client, resp.get_json()["token"] + + +def _register(server, username, email): + """Register one extra account on the test server.""" + client = server.app.test_client() + assert client.post("/api/register/send_code", json={"email": email}).status_code == 200 + code = server.sent[-1]["code"] + resp = client.post( + "/api/register", + json={ + "username": username, + "email": email, + "password": f"{username}s-password", + "code": code, + }, + ) + assert resp.status_code == 200, resp.get_json() + return resp.get_json()["user"] diff --git a/test/unit/transfer_web/test_mail_service.py b/test/unit/transfer_web/test_mail_service.py new file mode 100644 index 0000000..e1dfd1a --- /dev/null +++ b/test/unit/transfer_web/test_mail_service.py @@ -0,0 +1,163 @@ +"""Outgoing mailbox of the web server (verification codes). + +Registration, password reset and code login all depend on this service, so the +tests pin the contract the administrator relies on: settings are validated +against the real server before anything is stored, an unconfigured or corrupted +configuration never sends, and the delivered message carries the code. +""" + +import json +import os +import smtplib +import stat + +import pytest + +from PyFlow.transfer_web.web_backend.mail_service import ( + ENCRYPTION_MODES, + MailService, + normalize_config, +) + + +class FakeSmtp: + """Stand-in for an authenticated SMTP session.""" + + def __init__(self, fail_send=False): + self.messages = [] + self.fail_send = fail_send + self.quit_called = False + + def send_message(self, message): + if self.fail_send: + raise smtplib.SMTPException("mailbox unavailable") + self.messages.append(message) + + def quit(self): + self.quit_called = True + + def close(self): + self.quit_called = True + + +def valid_config(**overrides): + config = { + "host": "smtp.example.com", + "port": 465, + "username": "bot@example.com", + "password": "authorization-code", + "from": "PyFlow ", + "encryption": "ssl", + } + config.update(overrides) + return config + + +@pytest.fixture +def service(tmp_path): + return MailService(str(tmp_path / "email_config.json")) + + +def test_normalize_fills_defaults_and_checks_every_field(): + minimal = {"host": "h", "port": "587", "username": "u@x.com", "password": "p"} + assert normalize_config(minimal) == { + "host": "h", + "port": 587, + "username": "u@x.com", + "password": "p", + "from": "u@x.com", # the sender defaults to the mailbox account + "encryption": "ssl", + } + missing_sender = {"host": "h", "port": 465, "username": "x", "password": "p", "from": ""} + for bad in [ + valid_config(host=""), + valid_config(username=""), + valid_config(password=""), + missing_sender, # the sender falls back to the username, which is not an address + valid_config(port="not-a-port"), + valid_config(port=0), + valid_config(port=70000), + valid_config(encryption="plain"), + ]: + with pytest.raises(ValueError): + normalize_config(bad) + + +def test_encryption_modes_are_the_three_supported_ones(): + assert ENCRYPTION_MODES == ("ssl", "starttls", "none") + + +def test_service_is_disabled_without_a_configuration(service): + assert service.is_enabled() is False + assert service.get_config() is None + with pytest.raises(ValueError, match="not configured"): + service.send_code("alice@example.com", "123456", "register", 300) + + +def test_configure_validates_stores_and_enables_sending(service, tmp_path, monkeypatch): + session = FakeSmtp() + monkeypatch.setattr(service, "_connect", lambda config: session) + + stored = service.configure(valid_config()) + assert stored["port"] == 465 + assert service.is_enabled() is True + assert service.get_config()["host"] == "smtp.example.com" + assert session.quit_called is True # the probe connection is closed again + + written = json.loads((tmp_path / "email_config.json").read_text(encoding="utf-8")) + assert written == stored + if os.name == "posix": + assert stat.S_IMODE((tmp_path / "email_config.json").stat().st_mode) == 0o600 + + +def test_configure_rejects_settings_the_server_refuses(service, tmp_path, monkeypatch): + def refuse(config): + raise smtplib.SMTPAuthenticationError(535, b"bad credentials") + + monkeypatch.setattr(service, "_connect", refuse) + with pytest.raises(ValueError, match="rejected"): + service.configure(valid_config()) + assert service.is_enabled() is False + assert not (tmp_path / "email_config.json").exists() # nothing is stored + + +def test_a_stored_configuration_is_loaded_again(tmp_path, monkeypatch): + path = str(tmp_path / "email_config.json") + first = MailService(path) + monkeypatch.setattr(first, "_connect", lambda config: FakeSmtp()) + first.configure(valid_config()) + + assert MailService(path).is_enabled() is True + + +def test_a_corrupted_configuration_leaves_the_service_disabled(tmp_path): + path = tmp_path / "email_config.json" + path.write_text("{ not json", encoding="utf-8") + assert MailService(str(path)).is_enabled() is False + + path.write_text(json.dumps(valid_config(host="")), encoding="utf-8") + assert MailService(str(path)).is_enabled() is False + + +def test_send_code_delivers_the_code_with_its_purpose(service, monkeypatch): + session = FakeSmtp() + monkeypatch.setattr(service, "_connect", lambda config: session) + service.configure(valid_config()) + + service.send_code("alice@example.com", "654321", "reset_password", 300) + message = session.messages[0] + assert message["To"] == "alice@example.com" + assert message["From"] == "PyFlow " + assert "654321" in message.get_content() + assert "password change" in message.get_content() + assert "5 minutes" in message.get_content() + assert session.quit_called is True + + +def test_send_code_reports_delivery_failures(service, monkeypatch): + monkeypatch.setattr(service, "_connect", lambda config: FakeSmtp()) + service.configure(valid_config()) + monkeypatch.setattr(service, "_connect", lambda config: FakeSmtp(fail_send=True)) + + with pytest.raises(ValueError, match="sending the verification code"): + service.send_code("alice@example.com", "654321", "login", 300) diff --git a/test/unit/transfer_web/test_server_auth.py b/test/unit/transfer_web/test_server_auth.py new file mode 100644 index 0000000..044bbd1 --- /dev/null +++ b/test/unit/transfer_web/test_server_auth.py @@ -0,0 +1,246 @@ +"""Login gate, account store and role checks of the server web backend. + +The server web UI is the administration surface of the TCP server, so these +tests pin the security-relevant contract: anonymous visitors only get the +landing page, every administration endpoint needs a session (extensions and +configuration need an administrator), and the seeded ``admin``/``admin`` +account is hashed and flagged until its credentials are changed. +""" + +import contextlib +import os +import sqlite3 +import stat + +import pytest + +from PyFlow.transfer_web.web_backend import server_backend +from PyFlow.transfer_web.web_backend.user_database import UserDatabase + + +@pytest.fixture +def web(tmp_path, monkeypatch): + """A ServerWebApp whose account store and mail settings live under ``tmp_path``.""" + monkeypatch.setattr(server_backend, "FLOW_WEB_DIR", str(tmp_path)) + monkeypatch.setattr(server_backend, "SECRET_KEY_FILE", str(tmp_path / "web_secret_key")) + monkeypatch.setattr( + server_backend, "SERVER_EXTENSIONS_UI_FILE", str(tmp_path / "server_extensions_ui.json") + ) + app = server_backend.ServerWebApp( + db_path=str(tmp_path / "flow_web.db"), + mail_config_path=str(tmp_path / "email_config.json"), + ) + app.app.config.update(TESTING=True) + yield app + app.users.close() + + +@pytest.fixture +def client(web): + return web.app.test_client() + + +def login(client, identify="admin", password="admin"): + return client.post("/api/login", json={"identify": identify, "password": password}) + + +def add_user(client, username, password, email=None, role="user"): + return client.post( + "/api/users", + json={ + "username": username, + "email": email or f"{username}@example.com", + "password": password, + "role": role, + }, + ) + + +def page(client): + """The page an authenticated visitor gets, checked for the warning flag.""" + return client.get("/").get_data(as_text=True) + + +def test_landing_page_for_anonymous_visitors(client): + body = client.get("/").get_data(as_text=True) + assert "The PyFlow Server is running! Connect it in clients by the server host." in body + assert 'id="login-btn"' in body + assert 'id="register-btn"' in body # self-service registration + assert 'id="reset-btn"' in body # self-service password change + assert "Change Config" not in body # no administration UI before a login + + +def test_protected_endpoints_reject_anonymous_requests(client): + for method, path in [ + ("get", "/api/status"), + ("get", "/api/clients"), + ("get", "/api/events"), + ("get", "/api/extensions_ui"), + ("get", "/api/users"), + ("get", "/api/email_config"), + ("get", "/api/contact_requests"), + ("post", "/api/send_msg"), + ("post", "/api/save_config"), + ("post", "/api/users"), + ("post", "/api/run_extension"), + ("post", "/api/email_config"), + ("post", "/api/contacts/search"), + ]: + resp = getattr(client, method)(path, json={}) + assert resp.status_code == 401, (method, path) + assert client.get("/config").status_code == 302 # back to the landing page + + +def test_server_info_stays_public_for_clients(client): + # web clients discover the TCP address before they can log in + resp = client.get("/api/server_info") + assert resp.status_code != 401 + assert resp.status_code != 403 + + +def test_registration_stays_public_for_new_accounts(client): + # anyone who can reach the landing page may ask for a registration code + resp = client.post("/api/register/send_code", json={"email": "new@example.com"}) + assert resp.status_code == 400 # no mailbox configured yet, not a login failure + assert "mail service is not configured" in resp.get_json()["error"] + + +def test_default_admin_is_seeded_with_a_hashed_password(web, tmp_path): + with contextlib.closing(sqlite3.connect(tmp_path / "flow_web.db")) as conn: + rows = conn.execute("SELECT username, role, password FROM users").fetchall() + admin = next(row for row in rows if row[0] == "admin") + assert admin[1] == "admin" + assert admin[2].startswith("pbkdf2_sha256$") + assert "admin" not in admin[2] # the record is stored, never the password + if os.name == "posix": + assert stat.S_IMODE(os.stat(tmp_path / "flow_web.db").st_mode) == 0o600 + assert web.users.authenticate("admin", "admin")["role"] == "admin" + + +def test_login_with_default_credentials_warns_about_them(client): + data = login(client).get_json() + assert data["ok"] is True + assert data["role"] == "admin" + assert data["must_change_credentials"] is True + # the page hands the warning flag to the modal (PyFlowAccount.mount) + assert "mustChange: true" in page(client) + + +def test_login_accepts_the_email_of_an_account(client): + login(client) + add_user(client, "alice", "alices-password") + client.post("/api/logout") + + assert login(client, "alice@example.com", "alices-password").get_json()["username"] == "alice" + + +def test_login_rejects_a_wrong_password(client): + assert login(client, password="not-the-password").status_code == 401 + assert client.get("/api/status").status_code == 401 # and no session was created + + +def test_regular_user_gets_no_administration_access(client): + login(client) + assert add_user(client, "alice", "alices-password").status_code == 200 + client.post("/api/logout") + + data = login(client, "alice", "alices-password").get_json() + assert data["role"] == "user" + assert data["must_change_credentials"] is False + + assert client.get("/config").status_code == 302 + assert client.get("/api/users").status_code == 403 + assert client.get("/api/email_config").status_code == 403 + assert add_user(client, "mallory", "mallory-password").status_code == 403 + assert client.post("/api/run_extension", json={"command": "/anything"}).status_code == 403 + assert client.get("/api/status").status_code == 200 # normal functionality remains + + body = page(client) + assert 'id="users-btn"' not in body + assert 'id="add-ext-btn"' not in body + assert 'id="plus-btn"' not in body + assert "alice" in body # the account is shown in the sidebar footer + + +def test_removed_user_loses_access_immediately(web): + admin = web.app.test_client() + carol = web.app.test_client() + login(admin) + add_user(admin, "carol", "carols-password") + login(carol, "carol", "carols-password") + assert carol.get("/api/status").status_code == 200 + + assert admin.post("/api/users/delete", json={"username": "carol"}).status_code == 200 + assert carol.get("/api/status").status_code == 401 + + +def test_change_credentials_clears_the_warning(client): + login(client) + resp = client.post( + "/api/account", + json={ + "current_password": "admin", + "username": "root", + "password": "a-good-password", + "email": "root@example.com", + }, + ) + assert resp.get_json()["must_change_credentials"] is False + assert resp.get_json()["user"]["email"] == "root@example.com" + + client.post("/api/logout") + assert login(client, "admin", "admin").status_code == 401 # the default account is gone + assert login(client, "root", "a-good-password").get_json()["must_change_credentials"] is False + assert "mustChange: false" in page(client) + + +def test_changing_only_the_password_clears_the_warning(client): + login(client) + assert "mustChange: true" in page(client) + resp = client.post( + "/api/account", + json={"current_password": "admin", "username": "admin", "password": "a-good-password"}, + ) + assert resp.get_json()["must_change_credentials"] is False + assert "mustChange: false" in page(client) + + client.post("/api/logout") + assert login(client, "admin", "a-good-password").get_json()["must_change_credentials"] is False + + +def test_account_changes_require_the_current_password(client): + login(client) + resp = client.post( + "/api/account", + json={"current_password": "wrong", "username": "root", "password": "a-good-password"}, + ) + assert resp.status_code == 403 + # the rejected change left the account (and the warning) alone + assert "mustChange: true" in page(client) + + +def test_admin_cannot_remove_its_own_account(client): + login(client) + resp = client.post("/api/users/delete", json={"username": "admin"}) + assert resp.status_code == 400 + assert "own account" in resp.get_json()["error"] + + +def test_store_rejects_duplicates_short_passwords_and_orphaning_admins(web): + web.users.add_user("bob", "bob@example.com", "bobs-password") + with pytest.raises(ValueError): + web.users.add_user("bob", "other@example.com", "another-password") + with pytest.raises(ValueError): + web.users.add_user("shorty", "shorty@example.com", "abc") + with pytest.raises(ValueError): + web.users.remove_user("admin") # the only administrator + with pytest.raises(ValueError): + web.users.update_credentials("admin", new_username="bob", new_password="a-good-password") + + +def test_corrupt_store_does_not_restore_the_default_account(tmp_path): + (tmp_path / "flow_web.db").write_text("{ not a database", encoding="utf-8") + store = UserDatabase(str(tmp_path / "flow_web.db")) + with pytest.raises(ValueError): + store.authenticate("admin", "admin") # a damaged store never re-seeds + store.close() # the failed open released its connection already diff --git a/test/unit/transfer_web/test_user_database.py b/test/unit/transfer_web/test_user_database.py new file mode 100644 index 0000000..781c84e --- /dev/null +++ b/test/unit/transfer_web/test_user_database.py @@ -0,0 +1,337 @@ +"""Account, verification-code, contact and client-session store. + +The store is the single place where a server's accounts live, so these tests pin +its contract: unique usernames, emails and ids, codes that expire after five +minutes and may not be requested twice a minute, contacts that are mutual, and +client session tokens that stop working once dropped. +""" + +import contextlib +import json +import os +import re +import sqlite3 +import time + +import pytest + +from PyFlow.transfer_web.web_backend import user_database +from PyFlow.transfer_web.web_backend.user_database import ( + CODE_MAX_ATTEMPTS, + CODE_RESEND_SECONDS, + CODE_TTL_SECONDS, + UserDatabase, + mask_email, + validate_email, +) + + +@pytest.fixture +def store(tmp_path): + db = UserDatabase(str(tmp_path / "flow_web.db")) + yield db + db.close() + + +@contextlib.contextmanager +def _raw(db): + """Open the underlying database for white-box manipulation of timestamps.""" + connection = sqlite3.connect(db.path) + try: + yield connection + connection.commit() + finally: + connection.close() + + +def age_codes(db, seconds): + """Backdate stored codes so cooldown and expiry can be tested without waiting.""" + with _raw(db) as conn: + conn.execute( + "UPDATE verification_codes SET created_at = created_at - ?," + " expires_at = expires_at - ?", + (seconds, seconds), + ) + + +def test_new_store_seeds_the_default_admin(tmp_path): + db = UserDatabase(str(tmp_path / "flow_web.db")) + admin = db.find("admin") + assert admin["role"] == "admin" + assert re.fullmatch(r"[0-9A-F]{8}", admin["user_id"]) # ids may be all digits + assert db.authenticate("admin", "admin")["user_id"] == admin["user_id"] + db.close() + + +def test_register_assigns_a_unique_id_and_keeps_the_email_unique(store): + alice = store.register("alice", "Alice@Example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + assert alice["user_id"] != bob["user_id"] + assert len(alice["user_id"]) == 8 + assert alice["email"] == "Alice@Example.com" + assert alice["role"] == "user" + + # every identifier form finds the account, case-insensitively + assert store.find("ALICE")["user_id"] == alice["user_id"] + assert store.find("alice@example.com")["user_id"] == alice["user_id"] + assert store.find(alice["user_id"].lower())["user_id"] == alice["user_id"] + assert store.authenticate("alice@example.com", "alices-password")["username"] == "alice" + + with pytest.raises(ValueError): + store.register("alice", "other@example.com", "another-password") + with pytest.raises(ValueError): + store.register("other", "alice@example.com", "another-password") + with pytest.raises(ValueError): + store.register("shorty", "shorty@example.com", "abc") + + +def test_password_record_is_hashed_and_never_plaintext(store): + store.register("alice", "alice@example.com", "alices-password") + with _raw(store) as conn: + record = conn.execute("SELECT password FROM users WHERE username = 'alice'").fetchone()[0] + assert record.startswith("pbkdf2_sha256$") + assert "alices-password" not in record + assert store.authenticate("alice", "wrong-password") is None + assert store.authenticate("nobody", "alices-password") is None + + +def test_code_single_use_and_resend_cooldown(store): + issued = store.issue_code("register", "carol@example.com") + assert issued["expires_in"] == CODE_TTL_SECONDS == 300 + assert issued["resend_after"] == CODE_RESEND_SECONDS == 60 + + with pytest.raises(ValueError, match="wait"): + store.issue_code("register", "carol@example.com") + + with pytest.raises(ValueError, match="incorrect"): + store.verify_code("register", "carol@example.com", "000000" if issued["code"] != "000000" + else "111111") + + store.verify_code("register", "carol@example.com", issued["code"]) + with pytest.raises(ValueError, match="request a verification code first"): + store.verify_code("register", "carol@example.com", issued["code"]) # single use + + +def test_code_expires_after_five_minutes(store): + issued = store.issue_code("login", "carol@example.com") + age_codes(store, CODE_TTL_SECONDS + 1) + with pytest.raises(ValueError, match="expired"): + store.verify_code("login", "carol@example.com", issued["code"]) + + # the cooldown has passed as well, so a fresh code is issued + store.issue_code("login", "carol@example.com") + + +def test_code_attempt_limit(store): + issued = store.issue_code("login", "carol@example.com") + wrong = "000000" if issued["code"] != "000000" else "111111" + for _ in range(CODE_MAX_ATTEMPTS): + with pytest.raises(ValueError, match="incorrect"): + store.verify_code("login", "carol@example.com", wrong) + with pytest.raises(ValueError, match="too many wrong codes"): + store.verify_code("login", "carol@example.com", issued["code"]) + + +def test_codes_are_purposed_and_cannot_be_borrowed(store): + issued = store.issue_code("register", "carol@example.com") + with pytest.raises(ValueError, match="request a verification code first"): + store.verify_code("reset_password", "carol@example.com", issued["code"]) + with pytest.raises(ValueError, match="unknown verification purpose"): + store.issue_code("nonsense", "carol@example.com") + + +def test_discarding_a_code_frees_the_cooldown(store): + store.issue_code("register", "carol@example.com") + store.discard_codes("register", "carol@example.com") + store.issue_code("register", "carol@example.com") # an undeliverable code does not block + + +def test_register_with_code_spends_the_code_only_when_the_form_is_valid(store): + issued = store.issue_code("register", "carol@example.com") + with pytest.raises(ValueError, match="password"): + store.register_with_code("carol", "carol@example.com", "short", issued["code"]) + + # the rejected attempt left the code usable, and no account behind + assert store.find("carol") is None + carol = store.register_with_code( + "carol", "carol@example.com", "carols-password", issued["code"] + ) + assert carol["email"] == "carol@example.com" + assert store.email_registered("CAROL@example.com") is True + assert store.email_registered("dave@example.com") is False + + +def test_reset_password_with_code(store): + carol = store.register("carol", "carol@example.com", "carols-password") + issued = store.issue_code("reset_password", "carol@example.com") + + with pytest.raises(ValueError, match="incorrect"): + store.reset_password_with_code( + "carol", "000000" if issued["code"] != "000000" else "111111", "a-new-password" + ) + store.reset_password_with_code(carol["user_id"], issued["code"], "a-new-password") + assert store.authenticate("carol", "carols-password") is None + assert store.authenticate("carol", "a-new-password")["user_id"] == carol["user_id"] + + # an account without an email address cannot reset its password + with pytest.raises(ValueError, match="no email address"): + store.reset_password_with_code("admin", "123456", "another-password") + with pytest.raises(ValueError, match="no account matches"): + store.reset_password_with_code("nobody@example.com", "123456", "another-password") + + +def test_search_finds_accounts_by_every_identifier(store): + alice = store.register("alice", "alice@example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + + assert [u["username"] for u in store.search_users("ali", bob["user_id"])] == ["alice"] + assert [u["username"] for u in store.search_users("BOB@EXAMPLE", alice["user_id"])] == ["bob"] + found = store.search_users(bob["user_id"][:4], alice["user_id"]) + assert [u["username"] for u in found] == ["bob"] + assert store.search_users("alice", alice["user_id"]) == [] # never the searcher itself + + with pytest.raises(ValueError): + store.search_users(" ", alice["user_id"]) + with pytest.raises(ValueError): + store.search_users("x" * 65, alice["user_id"]) + + +def test_contacts_are_created_on_accept_and_are_mutual(store): + alice = store.register("alice", "alice@example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + + assert store.contacts(alice["user_id"]) == [] + store.request_contact(alice["user_id"], bob["user_id"]) + assert store.are_contacts(alice["user_id"], bob["user_id"]) is False # pending, not accepted + + requests = store.contact_requests(bob["user_id"]) + assert [r["user"]["username"] for r in requests["incoming"]] == ["alice"] + assert requests["outgoing"] == [] + assert store.contact_requests(alice["user_id"])["outgoing"][0]["user"]["username"] == "bob" + + requester = store.respond_request(bob["user_id"], requests["incoming"][0]["id"], True) + assert requester["username"] == "alice" + assert store.are_contacts(bob["user_id"], alice["user_id"]) is True + assert store.are_contacts(alice["user_id"], bob["user_id"]) is True + assert [c["username"] for c in store.contacts(alice["user_id"])] == ["bob"] + + with pytest.raises(ValueError, match="already answered"): + store.respond_request(bob["user_id"], requests["incoming"][0]["id"], True) + + +def test_request_contact_rejects_self_strangers_and_duplicates(store): + alice = store.register("alice", "alice@example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + carol = store.register("carol", "carol@example.com", "carols-password") + + with pytest.raises(ValueError, match="unknown user"): + store.request_contact(alice["user_id"], "ZZZZZZZZ") + with pytest.raises(ValueError, match="your own account"): + store.request_contact(alice["user_id"], alice["user_id"]) + + store.request_contact(alice["user_id"], bob["user_id"]) + with pytest.raises(ValueError, match="already pending"): + store.request_contact(alice["user_id"], bob["user_id"]) + + request_id = store.contact_requests(bob["user_id"])["incoming"][0]["id"] + store.respond_request(bob["user_id"], request_id, False) + assert store.are_contacts(alice["user_id"], bob["user_id"]) is False + assert store.contact_requests(bob["user_id"])["incoming"] == [] + + store.request_contact(alice["user_id"], bob["user_id"]) # a rejected request may be repeated + assert len(store.contact_requests(bob["user_id"])["incoming"]) == 1 + + with pytest.raises(ValueError, match="unknown contact request"): + store.respond_request(carol["user_id"], request_id, True) + + +def test_sessions_resolve_to_their_account_and_can_be_dropped(store): + alice = store.register("alice", "alice@example.com", "alices-password") + token = store.create_session(alice["user_id"]) + assert store.session_user(token)["username"] == "alice" + assert store.session_user("not-a-token") is None + assert store.session_user("") is None + + store.drop_session(token) + assert store.session_user(token) is None + with pytest.raises(ValueError): + store.create_session("ZZZZZZZZ") + + +def test_removing_an_account_takes_its_contacts_and_sessions_with_it(store): + alice = store.register("alice", "alice@example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + store.request_contact(alice["user_id"], bob["user_id"]) + request_id = store.contact_requests(bob["user_id"])["incoming"][0]["id"] + store.respond_request(bob["user_id"], request_id, True) + bob_token = store.create_session(bob["user_id"]) + + store.remove_user("alice") + assert store.find("alice") is None + assert store.contacts(bob["user_id"]) == [] + assert store.contact_requests(bob["user_id"])["incoming"] == [] + + store.remove_user(bob["user_id"]) + assert store.session_user(bob_token) is None + + +def test_update_credentials_can_change_the_email(store): + alice = store.register("alice", "alice@example.com", "alices-password") + bob = store.register("bob", "bob@example.com", "bobs-password") + + updated = store.update_credentials("alice", new_username="alice2", email="alice2@example.com") + assert (updated["username"], updated["email"]) == ("alice2", "alice2@example.com") + relogin = store.authenticate("alice2@example.com", "alices-password") + assert relogin["user_id"] == alice["user_id"] + + with pytest.raises(ValueError, match="already registered"): + store.update_credentials("alice2", email=bob["email"]) + with pytest.raises(ValueError, match="already exists"): + store.update_credentials("alice2", new_username="bob") + with pytest.raises(ValueError, match="unknown user"): + store.update_credentials("nobody", new_password="another-password") + + +def test_legacy_users_json_is_imported_once_and_archived(tmp_path): + legacy = { + "users": [ + {"username": "admin", "role": "admin", "password": "pbkdf2_sha256$1$salt$deadbeef"}, + {"username": "ghost", "role": "user", "password": ""}, # incomplete, skipped + ] + } + (tmp_path / "users.json").write_text(json.dumps(legacy), encoding="utf-8") + + db = UserDatabase(str(tmp_path / "flow_web.db")) + assert [u["username"] for u in db.list_users()] == ["admin"] + assert db.find("admin")["email"] is None + assert (tmp_path / "users.json.migrated").exists() + assert not (tmp_path / "users.json").exists() + db.close() + + +def test_validate_email_and_mask_email(): + assert validate_email(" someone@example.com ") == "someone@example.com" + for bad in ["", "no-at-sign", "a@b", "@example.com", "a b@example.com"]: + with pytest.raises(ValueError): + validate_email(bad) + assert mask_email("alice@example.com") == "a***@example.com" + assert mask_email("") == "" + assert mask_email(None) == "" + + +@pytest.mark.skipif(os.name != "posix", reason="file modes are a POSIX concept") +def test_database_file_is_not_world_readable(tmp_path): + db = UserDatabase(str(tmp_path / "flow_web.db")) + mode = (tmp_path / "flow_web.db").stat().st_mode + assert mode & 0o077 == 0 + db.close() + + +def test_codes_are_stored_hashed(store): + issued = store.issue_code("register", "carol@example.com") + with _raw(store) as conn: + stored = conn.execute("SELECT code_hash, expires_at FROM verification_codes").fetchone() + assert stored[0] != issued["code"] + assert issued["code"] not in stored[0] + assert stored[1] <= time.time() + CODE_TTL_SECONDS + assert user_database.CODE_DIGITS == 6 diff --git a/test/unit/transfer_web/test_web_ftp.py b/test/unit/transfer_web/test_web_ftp.py new file mode 100644 index 0000000..d612268 --- /dev/null +++ b/test/unit/transfer_web/test_web_ftp.py @@ -0,0 +1,479 @@ +"""The web "ftp" share: server-side sharing/listing and the client browse path. + +This is not the FTP protocol: the server exposes one folder of its own host, the +client asks for a listing over the protocol's own commands and the picked +entries are pushed with the native ``/file`` and ``/file_folder`` transfers. The +tests use the fake TCP instances of the neighbouring web test modules, so no +socket is opened and no crypto library is needed. +""" + +import json +import os +import threading + +import pytest + +from PyFlow.transfer_web.web_backend import server_backend +from PyFlow.transfer_web.web_front import client_backend + + +class FakeSocket: + """Socket stand-in exposing the bound address of the client.""" + + def getsockname(self): + """Return the local address of the pretend connection.""" + return ("127.0.0.1", 40000) + + +class FakeTcpServer: + """TCP_Server_Base stand-in recording registrations, messages and pushes.""" + + def __init__(self, clients=None): + self.running = True + self.host = "127.0.0.1" + self.port = 65432 + self.is_enable_encrypto = False + self.clients = dict(clients or {}) + self.client_lock = threading.Lock() + self.file_transfer_dir = "/tmp" + self.commands = {} # (where_to_run, name) -> handler + self.pushes = [] # ("file"|"folder", command) + self.messages = [] # (socket, text) + + def register_command(self, name, handler, where_to_run="server", run_in_thread=False): + """Record one registered command.""" + self.commands[(where_to_run, name)] = handler + + def send_message(self, sock, message): + """Record one line written to a client.""" + self.messages.append((sock, message)) + return True + + def file_transfer_server_recv_client_start(self, message, file_folder_abspath=None): + """Record one file push.""" + self.pushes.append(("file", message)) + + def folder_file_transfer_server_recv_client_start(self, message): + """Record one folder push.""" + self.pushes.append(("folder", message)) + + def stop(self): + """Drop the pretend connection.""" + self.running = False + + +class FakeTcpClient: + """TCP_Client_Base stand-in that answers commands like the server would. + + ``replier(message)`` returns the reply line the server would send back (or + None); the reply is dispatched to the handler this client registered for + that command, exactly like the receive thread does on a real connection. + """ + + def __init__(self, replier=None): + self.running = True + self.is_enable_encrypto = False + self.is_extend_command = False + self.is_custom_keys = None + self.is_input_command_in_console = False + self.is_wait_server = True + self.host = "127.0.0.1" + self.port = 65432 + self.client_host = "127.0.0.1" + self.client_port = 40000 + self.timeout = None + self.port_add_step = 1 + self.max_thread_num = 10 + self.max_mem_buff = 2048 * 1024 * 1024 + self.client_socket = FakeSocket() + self._crypto_lock = threading.Lock() + self._encrypted_sockets = set() + self.sent = [] + self.commands = {} + self.replier = replier + + def register_command(self, name, handler, where_to_run="server", run_in_thread=False): + """Record one handler for a line pushed by the server.""" + self.commands[name] = handler + + def send_message(self, client_socket, message): + """Record one command and let the fake server answer it.""" + self.sent.append(message) + reply = self.replier(message) if self.replier else None + if reply: + for name, handler in self.commands.items(): + if reply == name or reply.startswith(name + " "): + handler(self.client_socket, ("127.0.0.1", 65432), reply) + break + return True + + def close(self): + """Drop the pretend connection.""" + self.running = False + + +def _request_id(line): + """Request id of one ``/ `` line.""" + return line.split(" ", 2)[1] + + +@pytest.fixture +def web(tmp_path, monkeypatch): + """A ServerWebApp with a fake TCP server and its files under ``tmp_path``.""" + monkeypatch.setattr(server_backend, "FLOW_WEB_DIR", str(tmp_path)) + monkeypatch.setattr(server_backend, "SECRET_KEY_FILE", str(tmp_path / "web_secret_key")) + monkeypatch.setattr(server_backend, "SERVER_CONFIG_FILE", str(tmp_path / "setup_server.json")) + app = server_backend.ServerWebApp( + db_path=str(tmp_path / "flow_web.db"), + mail_config_path=str(tmp_path / "email_config.json"), + ) + app.app.config.update(TESTING=True) + app.server = FakeTcpServer() + app.mode = "status" + app._register_ftp_commands() + yield app + app.users.close() + + +@pytest.fixture +def client(web): + return web.app.test_client() + + +@pytest.fixture +def share(tmp_path): + """A shared folder holding one file and one subfolder with one file.""" + root = tmp_path / "share" + (root / "sub").mkdir(parents=True) + (root / "alpha.txt").write_text("alpha", encoding="utf-8") + (root / "sub" / "beta.bin").write_bytes(b"beta") + return root + + +@pytest.fixture +def client_web(tmp_path, monkeypatch): + """A ClientWebApp with a fake TCP client; returns ``(app, flask client)``.""" + monkeypatch.setattr(client_backend, "FLOW_WEB_DIR", str(tmp_path)) + monkeypatch.setattr(client_backend, "CLIENT_LOGIN_FILE", str(tmp_path / "client_login.json")) + monkeypatch.setattr( + client_backend, "CLIENT_LAST_SERVER_FILE", str(tmp_path / "client_last_server.json") + ) + monkeypatch.setattr(client_backend, "CLIENT_CONFIG_FILE", str(tmp_path / "setup_client.json")) + monkeypatch.setattr( + client_backend, "CLIENT_EXTENSIONS_UI_FILE", str(tmp_path / "client_extensions_ui.json") + ) + app = client_backend.ClientWebApp(web_port=5099) + app.app.config.update(TESTING=True) + app.connected = True + app.client = FakeTcpClient() + app._register_ftp_commands() + return app, app.app.test_client() + + +def login(client, identify="admin", password="admin"): + return client.post("/api/login", json={"identify": identify, "password": password}) + + +# ---- configuration flags ----------------------------------------------------- + + +def test_config_forms_list_the_new_flags(web, client, client_web): + """Every new switch is editable in the web startup configuration.""" + server_keys = [key for key, *_ in server_backend.SERVER_PARAM_FIELDS] + client_keys = [key for key, *_ in client_backend.CLIENT_PARAM_FIELDS] + assert "is_asynic_clients_io" in server_keys + assert "is_debug" in server_keys and "is_print_log" in server_keys + assert "is_debug" in client_keys and "is_print_log" in client_keys + # the client class has no coroutine mode: offering it would fail validation + assert "is_asynic_clients_io" not in client_keys + + login(client) + server_form = client.get("/config").get_data(as_text=True) + for key in ("is_asynic_clients_io", "is_debug", "is_print_log"): + assert f'id="f-{key}"' in server_form, key + client_app, client_flask = client_web + client_form = client_flask.get("/config").get_data(as_text=True) + for key in ("is_debug", "is_print_log"): + assert f'id="f-{key}"' in client_form, key + assert 'id="f-is_asynic_clients_io"' not in client_form + + +# ---- server side ------------------------------------------------------------- + + +def test_share_starts_empty_and_needs_an_admin(client, tmp_path): + """Nothing is shared until an administrator picks a folder.""" + assert client.get("/api/ftp").status_code == 401 # anonymous + login(client) + body = client.get("/api/ftp").get_json() + assert body == {"ok": True, "root": None, "shared": False} + assert client.get("/api/ftp/list").status_code == 404 + added = client.post("/api/ftp/add", json={"path": str(tmp_path)}) # admin + assert added.status_code == 200 + + +def test_add_and_remove_the_shared_folder(web, client, share): + """Adding a folder shares it, removing it takes the share away.""" + login(client) + missing = client.post("/api/ftp/add", json={"path": str(share / "nope")}) + assert missing.status_code == 400 + assert "not a folder" in missing.get_json()["error"] + + added = client.post("/api/ftp/add", json={"path": str(share)}) + assert added.status_code == 200 + assert added.get_json()["root"] == str(share) + status = client.get("/api/ftp").get_json() + assert status["shared"] is True and status["root"] == str(share) + # both protocol commands are registered on the running TCP server + assert ("server", server_backend.FTP_LIST_COMMAND) in web.server.commands + assert ("server", server_backend.FTP_GET_COMMAND) in web.server.commands + + listing = client.get("/api/ftp/list").get_json()["listing"] + assert listing["path"] == "" and listing["parent"] is None + assert [e["name"] for e in listing["entries"]] == ["sub", "alpha.txt"] # folders first + assert listing["entries"][1]["size"] == 5 # noqa: PLR2004 + assert client.get("/api/ftp/list?path=sub").get_json()["listing"]["parent"] == "" + + assert client.post("/api/ftp/remove").get_json() == {"ok": True} + assert client.get("/api/ftp").get_json()["shared"] is False + + +def test_listing_refuses_paths_outside_the_share(web, client, share): + """A share-relative path can never walk out of the shared folder.""" + login(client) + client.post("/api/ftp/add", json={"path": str(share)}) + for escape in ("..", "../..", str(share.parent), "/etc"): + response = client.get(f"/api/ftp/list?path={escape}") + assert response.status_code == 400, escape + handler = web.server.commands[("server", server_backend.FTP_LIST_COMMAND)] + reply = handler(None, ("127.0.0.1", 5000), f"{server_backend.FTP_LIST_COMMAND} 9 ..") + assert reply.startswith(f"{server_backend.FTP_ERROR_COMMAND} 9 ") + + +def test_protocol_listing_handler_answers_the_client(web, share): + """``/ftp_list`` answers with the JSON listing of the requested folder.""" + handler = web.server.commands[("server", server_backend.FTP_LIST_COMMAND)] + reply = handler(None, ("127.0.0.1", 5000), f"{server_backend.FTP_LIST_COMMAND} 3 ") + assert reply.startswith(f"{server_backend.FTP_ERROR_COMMAND} 3 no folder is shared") + + with web._ftp_lock: + web.ftp_root = str(share) + # the web client sends the folder as a JSON string; a raw path works too + json_path = f'{server_backend.FTP_LIST_COMMAND} 4 "sub"' + raw_path = f"{server_backend.FTP_LIST_COMMAND} 5 sub" + for request in (json_path, raw_path): + reply = handler(None, ("127.0.0.1", 5000), request) + command, _, payload = reply.split(" ", 2) + assert command == server_backend.FTP_LIST_OK_COMMAND, reply + listing = json.loads(payload) + assert listing["path"] == "sub" and listing["parent"] == "" + assert [e["name"] for e in listing["entries"]] == ["beta.bin"] + root_reply = handler(None, ("127.0.0.1", 5000), f'{server_backend.FTP_LIST_COMMAND} 6 ""') + assert json.loads(root_reply.split(" ", 2)[2])["path"] == "" + + +def test_protocol_download_handler_pushes_the_selection(web, share): + """``/ftp_get`` pushes files and folders with the native transfer commands.""" + handler = web.server.commands[("server", server_backend.FTP_GET_COMMAND)] + address = ("127.0.0.1", 41000) + web.server.clients[address] = {"socket": FakeSocket(), "address": address} + with web._ftp_lock: + web.ftp_root = str(share) + + reply = handler( + None, + address, + f"{server_backend.FTP_GET_COMMAND} 1 {json.dumps(['alpha.txt', 'sub', '../escape'])}", + ) + assert reply == f"{server_backend.FTP_GET_OK_COMMAND} 1 2 1" + kinds = [kind for kind, _cmd in web.server.pushes] + assert kinds == ["file", "folder"] + assert str(share / "alpha.txt") in web.server.pushes[0][1] + assert str(share / "sub") in web.server.pushes[1][1] + assert "41000" in web.server.pushes[0][1] # addressed to the asking client + + unknown = ("127.0.0.1", 41001) + assert handler(None, unknown, f"{server_backend.FTP_GET_COMMAND} 2 []").startswith( + server_backend.FTP_ERROR_COMMAND + ) + + +def test_protocol_download_handler_honours_the_destination(web, share): + """A client-chosen download folder travels on the native transfer commands.""" + handler = web.server.commands[("server", server_backend.FTP_GET_COMMAND)] + address = ("127.0.0.1", 41000) + web.server.clients[address] = {"socket": FakeSocket(), "address": address} + with web._ftp_lock: + web.ftp_root = str(share) + + request = {"paths": ["alpha.txt", "sub"], "destination": "/tmp/picked"} + reply = handler(None, address, f"{server_backend.FTP_GET_COMMAND} 5 {json.dumps(request)}") + assert reply == f"{server_backend.FTP_GET_OK_COMMAND} 5 2 0" + assert [kind for kind, _cmd in web.server.pushes] == ["file", "folder"] + assert all("/tmp/picked" in command for _kind, command in web.server.pushes) + + # an empty destination keeps the receiver's default transfer folder + web.server.pushes.clear() + request = {"paths": ["alpha.txt"], "destination": " "} + reply = handler(None, address, f"{server_backend.FTP_GET_COMMAND} 6 {json.dumps(request)}") + assert reply == f"{server_backend.FTP_GET_OK_COMMAND} 6 1 0" + assert "/tmp/picked" not in web.server.pushes[0][1] + + malformed = {"paths": "alpha.txt", "destination": "/tmp/picked"} + reply = handler(None, address, f"{server_backend.FTP_GET_COMMAND} 7 {json.dumps(malformed)}") + assert reply == f"{server_backend.FTP_ERROR_COMMAND} 7 malformed request" + + +def test_shared_folder_is_persisted_and_restored(web, client, share, tmp_path, monkeypatch): + """Adding a share records it in the startup config; a restart restores it.""" + login(client) + assert client.post("/api/ftp/add", json={"path": str(share)}).status_code == 200 + config_path = tmp_path / "setup_server.json" + saved = json.loads(config_path.read_text(encoding="utf-8")) + assert saved["web"]["ftp_root"] == str(share) + + # a restart with a saved server entry brings the share back + saved["servers"] = [{"host": "127.0.0.1", "port": 65432}] + config_path.write_text(json.dumps(saved), encoding="utf-8") + restarted = server_backend.ServerWebApp( + db_path=str(tmp_path / "restart.db"), mail_config_path=str(tmp_path / "mail.json") + ) + started = [] + monkeypatch.setattr(restarted, "_start_server", started.append) + restarted.start_from_config() + assert started == [{"host": "127.0.0.1", "port": 65432}] + assert restarted._ftp_shared_root() == str(share) + restarted.users.close() + + assert client.post("/api/ftp/remove").get_json() == {"ok": True} + assert "ftp_root" not in json.loads(config_path.read_text(encoding="utf-8"))["web"] + + +def test_saving_the_startup_config_keeps_the_shared_folder(web, client, share, monkeypatch): + """Re-saving the TCP parameters does not drop the shared "ftp" folder.""" + login(client) + assert client.post("/api/ftp/add", json={"path": str(share)}).status_code == 200 + started = [] + monkeypatch.setattr(web, "_start_server", started.append) + web._bound_port = 5000 + response = client.post( + "/api/save_config", + json={"params": {"host": "127.0.0.1", "port": 65432}, "web_port": 5000}, + ) + assert response.status_code == 200 + assert started == [{"host": "127.0.0.1", "port": 65432}] + saved = server_backend._read_config_file() + assert saved["servers"] == [{"host": "127.0.0.1", "port": 65432}] + assert saved["web"] == {"port": 5000, "ftp_root": str(share)} + + +# ---- client side ------------------------------------------------------------- + + +def _server_reply(web, shared=True, started=2): + """A replier answering the client's ftp commands like the real server.""" + + def reply(line): + if line.startswith(client_backend.FTP_LIST_COMMAND): + if not shared: + return f"{client_backend.FTP_ERROR_COMMAND} {_request_id(line)} no folder is shared" + rel = json.loads(line.split(" ", 2)[2]) + listing = _fake_listing(rel) + return f"{client_backend.FTP_LIST_OK_COMMAND} {_request_id(line)} {json.dumps(listing)}" + if line.startswith(client_backend.FTP_GET_COMMAND): + return f"{client_backend.FTP_GET_OK_COMMAND} {_request_id(line)} {started} 0" + return None + + return reply + + +def _fake_listing(rel): + return { + "path": rel, + "parent": None if rel == "" else "", + "entries": [ + {"name": "sub", "dir": True, "size": 0, "mtime": 1700000000}, + {"name": "alpha.txt", "dir": False, "size": 5, "mtime": 1700000000}, + ], + } + + +def test_client_lists_the_server_share(client_web): + """The client backend turns one protocol round trip into the listing.""" + app, client = client_web + app.client.replier = _server_reply(app, shared=True) + response = client.post("/api/ftp/list", json={"path": ""}) + assert response.status_code == 200 + listing = response.get_json()["listing"] + assert [e["name"] for e in listing["entries"]] == ["sub", "alpha.txt"] + assert app.client.sent[0].startswith(client_backend.FTP_LIST_COMMAND + " 1 ") + assert json.loads(app.client.sent[0].split(" ", 2)[2]) == "" # the path travels as JSON + + +def test_client_downloads_the_selection(client_web): + """The picked entries are handed to the server's transfer commands.""" + app, client = client_web + app.client.replier = _server_reply(app, shared=True, started=3) + response = client.post( + "/api/ftp/download", json={"paths": ["alpha.txt", "sub", "sub/beta.bin"]} + ) + assert response.status_code == 200 + assert response.get_json() == {"ok": True, "started": 3, "skipped": 0} + sent = app.client.sent[0] + assert sent.startswith(client_backend.FTP_GET_COMMAND + " ") + assert json.loads(sent.split(" ", 2)[2]) == { + "paths": ["alpha.txt", "sub", "sub/beta.bin"], + "destination": "", + } + + +def test_client_download_passes_the_destination(client_web): + """A chosen download folder reaches the server; an empty one is dropped.""" + app, client = client_web + app.client.replier = _server_reply(app, shared=True, started=1) + response = client.post( + "/api/ftp/download", json={"paths": ["alpha.txt"], "destination": " /tmp/picked "} + ) + assert response.status_code == 200 + payload = json.loads(app.client.sent[0].split(" ", 2)[2]) + assert payload == {"paths": ["alpha.txt"], "destination": "/tmp/picked"} + + +def test_client_download_needs_a_selection(client_web): + """An empty selection is refused before anything is sent.""" + app, client = client_web + assert client.post("/api/ftp/download", json={"paths": []}).status_code == 400 + assert app.client.sent == [] + + +def test_client_reports_server_refusals_and_timeouts(client_web, monkeypatch): + """A refusal and a silent server both surface as HTTP errors.""" + app, client = client_web + app.client.replier = _server_reply(app, shared=False) + refused = client.post("/api/ftp/list", json={"path": ""}) + assert refused.status_code == 502 + assert "no folder is shared" in refused.get_json()["error"] + + app.client.replier = None # the server never answers + monkeypatch.setattr(client_backend, "REQUEST_TIMEOUT", 0.2) + silent = client.post("/api/ftp/list", json={"path": ""}) + assert silent.status_code == 504 + + app.connected = False + assert client.post("/api/ftp/list", json={"path": ""}).status_code == 503 + + +def test_ftp_helpers_resolve_and_refuse(tmp_path): + """The share helpers are the single gate for every listing and download.""" + root = tmp_path / "root" + root.mkdir() + inside = root / "a" + inside.write_text("x", encoding="utf-8") + assert server_backend._ftp_resolve(str(root), "a")[1] == "a" + assert server_backend._ftp_listing(str(root), "")["entries"][0]["name"] == "a" + for bad in ("..", "/etc", "a/../../b"): + with pytest.raises(ValueError): + server_backend._ftp_resolve(str(root), bad) + with pytest.raises(ValueError): + server_backend._ftp_listing(str(root), "a") # a file is not a folder + os.makedirs(root / "b")