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

213 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
---