213 lines
6.1 KiB
Markdown
213 lines
6.1 KiB
Markdown
# 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.
|
||
|
||
---
|