# 04 — Uplink: Hash a Local File from the Cloud **Goal:** Let your Anvil app call a **local Python function** (via Uplink) to compute a file’s SHA-256 hash. When Uplink is running, it works; when it isn’t, the UI shows “Uplink offline.” --- ## 1) What You’ll Build - A client UI with a **FileLoader** and a **Label** for status/result. - A **server callable** the client invokes (so calls always go client → server). - A **local Uplink script** that actually computes the hash. - Graceful error handling when the Uplink agent is offline. --- ## 2) Anvil App UI On `Form1` (client): - Add **FileLoader** → `file_loader_1` (text: “Choose file…”). - Add **Label** → `label_status` (text: “Uplink: checking…”, foreground: muted). - Add **TextArea** → `text_area_result` (read-only, multi-line; for hash + meta). Optional: add a **Button** “Copy hash” bound to `text_area_result.text`. --- ## 3) Server Module Create or open `ServerModule1` and add: ```python import anvil.server from anvil import media @anvil.server.callable def hash_selected_file(file_media: media.Media): """ Proxies the request to an Uplink-connected function 'hash_file'. Returns a dict with sha256, name, size, and error (if any). """ try: if not file_media: return {"error": "No file provided."} # This calls the uplink-exposed function defined in the local script below. result = anvil.server.call('hash_file', file_media) # Expecting uplink to return a dict with sha256 and size (bytes). return { "name": getattr(file_media, 'name', 'unknown'), "size": getattr(file_media, 'size', None), **(result or {}) } except anvil.server.TimeoutError: return {"error": "Uplink timeout — is the local agent running?"} except anvil.server.NoServerFunctionError: return {"error": "Uplink function not found. Did you name it 'hash_file'?"} except Exception as e: return {"error": f"Unexpected error: {e.__class__.__name__}: {e}"} ```` > Why a server proxy? It keeps your client thin and lets you add auth/role checks later without touching the UI. --- ## 4) Client Code (Form1) Open **Code** for `Form1` and add: ```python from anvil import * import anvil.server class Form1(Form1Template): def __init__(self, **properties): self.init_components(**properties) self.label_status.text = "Uplink: checking…" self._check_uplink_status() def _check_uplink_status(self): """ Calls a tiny ping if available; otherwise assume offline. You can skip this and just infer from failures, but UX is nicer with a badge. """ try: # If you create a 'uplink_ping' callable in the uplink script, uncomment: # pong = anvil.server.call('uplink_ping') # self.label_status.text = f"Uplink: {pong}" # For now, we’ll just set neutral. self.label_status.text = "Uplink: ready (if agent is running)" except Exception: self.label_status.text = "Uplink: offline" def file_loader_1_change(self, file, **event_args): """Called when a new file is loaded into this FileLoader""" if not file: return self.label_status.text = "Hashing…" self.file_loader_1.enabled = False try: res = anvil.server.call('hash_selected_file', file) if res.get("error"): Notification(res["error"], style="danger", timeout=4).show() self.label_status.text = "Uplink: offline or error" return name = res.get("name") or getattr(file, "name", "unknown") size = res.get("size") or getattr(file, "size", None) sha = res.get("sha256") size_kb = f"{(size/1024):.1f} KB" if isinstance(size, (int, float)) else "n/a" self.text_area_result.text = ( f"File: {name}\n" f"Size: {size_kb}\n" f"SHA-256:\n{sha}" ) self.label_status.text = "Uplink: online ✓" finally: self.file_loader_1.enabled = True ``` --- ## 5) Local Uplink Script Create a local Python file on your machine, e.g. `uplink_hash.py`. Install the Uplink package if needed: ```bash pip install anvil-uplink ``` Script: ```python # uplink_hash.py import hashlib import anvil.server # --- connect --- # Replace with your key from Anvil (Settings -> Uplink) anvil.server.connect("YOUR-UPLINK-KEY") # --- functions --- @anvil.server.callable("uplink_ping") def uplink_ping(): return "online ✓" @anvil.server.callable("hash_file") def hash_file(file_media): """ file_media is an anvil.Media object coming from the client. We'll read it as bytes and return SHA-256. """ # .get_bytes() gives a Bytes object; .get_bytes_io() gives a BytesIO b = file_media.get_bytes() h = hashlib.sha256(b).hexdigest() return {"sha256": h, "size": len(b)} # --- stay alive --- anvil.server.wait_forever() ``` Run it locally: ```bash python uplink_hash.py ``` Leave the terminal open while you use the app. If you stop it, the app should show **Uplink: offline** behavior. --- ## 6) Security Notes (read these) * **Never** log or expose full file contents; we only compute a hash. * Uplink runs **on your machine**; it can read files if *you* write code to do so. Treat it like any local service. * If you add roles (Tutorial 05), gate the server proxy (`hash_selected_file`) behind a role like `viewer` or `editor`. --- ## 7) Troubleshooting * **`NoServerFunctionError`**: Function name mismatch. You must expose `hash_file` and call exactly that. * **Timeout**: Local script not running or blocked by network. Try a simple `uplink_ping` to confirm. * **Huge files**: This demo reads all bytes. For large files, stream in chunks or limit size on client. --- ## 8) Demo Asset Record a **10–15s GIF**: 1. Start uplink script. 2. Choose a small file. 3. Show hash result and “Uplink: online ✓”. 4. Stop uplink; try again to show error state. Save as: `assets/uplink-hash-demo.gif` Reference here: ```markdown ![Uplink Hash Demo](../assets/uplink-hash-demo.gif) ``` --- ## 9) Takeaways * Uplink lets your cloud app do **local** work securely. * Keep a thin server proxy for auth + error shaping. * Detect offline state and tell the user *why* it failed. ---