Add tutorials/04-uplink-file-hash.md
feat(tutorials): add 04 Uplink file-hash guide (server proxy)
This commit is contained in:
parent
ec407aae5b
commit
56f2560787
|
@ -0,0 +1,212 @@
|
|||
# 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
|
||||

|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
Loading…
Reference in New Issue