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