FluidFrameDev/tutorials/04-uplink-file-hash.md

213 lines
6.1 KiB
Markdown
Raw Normal View History

# 04 — Uplink: Hash a Local File from the Cloud
**Goal:** Let your Anvil app call a **local Python function** (via Uplink) to compute a files SHA-256 hash. When Uplink is running, it works; when it isnt, the UI shows “Uplink offline.”
---
## 1) What Youll 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, well 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 **1015s 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.
---