-
-
Notifications
You must be signed in to change notification settings - Fork 687
feat: share an album beyond the local network #1478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rohan-pandeyy
merged 26 commits into
AOSSIE-Org:main
from
rohan-pandeyy:feat/share-internet-mode
Aug 9, 2026
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
979513c
build: enable tokio's time feature
rohan-pandeyy 3c8b02f
feat: open an ssh reverse tunnel to the share server
rohan-pandeyy 4907530
feat: register the tunnel commands and close it on exit
rohan-pandeyy b927614
fix: stop reporting a tunnel that has died
rohan-pandeyy 63f3674
feat: add the tunnel bridge to the frontend
rohan-pandeyy 476d624
feat: let a share be opened to the internet
rohan-pandeyy 9a466b6
test: cover internet mode in the share dialog
rohan-pandeyy d378601
docs: explain what sharing an album actually does
rohan-pandeyy 8599366
feat: link the share dialog to the documentation
rohan-pandeyy d24c5c5
test: cover the documentation link
rohan-pandeyy 7d429f5
fix: say something when the docs link cannot be opened
rohan-pandeyy e4fbf57
fix: allow the docs URL through the opener scope
rohan-pandeyy 470ffbe
style: sit the help button next to the share title
rohan-pandeyy 5d305c8
docs: tighten the sharing page opening
rohan-pandeyy 1bd9422
fix: serialise the tunnel lifecycle and declare tokio sync
rohan-pandeyy 784ec82
fix: stop the tunnel on every exit path
rohan-pandeyy e4ce4ea
fix: base tunnel cleanup on the tunnel, not on dialog state
rohan-pandeyy a900d1c
test: cover the tunnel cleanup and rollback failures
rohan-pandeyy e2f7576
docs: correct the privacy claims and drop the sample token
rohan-pandeyy e6da2f0
fix: track the ssh child before it announces a URL
rohan-pandeyy f37316b
fix: stop a late status lookup clearing a newer tunnel
rohan-pandeyy 6b77301
test: cover the late status lookup race
rohan-pandeyy 701eb91
remove em dashes
rohan-pandeyy b35d69c
fix: refuse to track an ssh child once shutdown has begun
rohan-pandeyy 21430eb
fix: stop a tunnel that opened after it was closed
rohan-pandeyy 7182699
test: wait past the mutation retry backoff for the error dialog
rohan-pandeyy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| # Album Sharing over the Local Network | ||
|
|
||
| PictoPy can serve a single album over the local network so that someone on the same Wi-Fi can browse it in a normal browser. Nothing is uploaded anywhere — the photos are streamed straight from the host machine, and the share disappears when PictoPy closes. | ||
|
|
||
| This page is for developers: running the backend yourself, driving the share API by hand, and understanding how the pieces fit. If you are looking for what sharing means for your photos and your privacy, read [Sharing Albums](../../overview/sharing-albums.md) instead. | ||
|
|
||
| Sharing an album from the desktop app is a menu item on the album card; everything below describes the machinery underneath it. | ||
|
|
||
| ## Running the backend | ||
|
|
||
| Set up the environment once by following the [Manual Setup Guide](../../Manual_Setup_Guide.md) — create the environment, activate it, and `pip install -r requirements.txt` inside `backend/`. | ||
|
|
||
| After that there are two ways to start the server, both from the `backend/` directory: | ||
|
|
||
| ```bash | ||
| python main.py | ||
| ``` | ||
|
|
||
| This is the plain entry point. It binds `localhost:52123` and is exactly what the packaged desktop app runs, so it is the closest match to production behaviour. | ||
|
|
||
| ```bash | ||
| fastapi dev --port 52123 | ||
| ``` | ||
|
|
||
| This adds auto-reload, which is nicer while editing. Both work with album sharing. | ||
|
|
||
| Check it came up: | ||
|
|
||
| ```bash | ||
| curl http://localhost:52123/health | ||
| ``` | ||
|
|
||
| ### If `fastapi dev` crashes on Windows | ||
|
|
||
| You may see `UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f40d'`. The FastAPI CLI prints an emoji in its banner and the default Windows console codepage cannot encode it. This is unrelated to PictoPy. Either use `python main.py`, or set the encoding first: | ||
|
|
||
| ```bash | ||
| set PYTHONIOENCODING=utf-8 | ||
| ``` | ||
|
|
||
| ## How sharing is wired | ||
|
|
||
| Three listeners, and only one of them is reachable from the network: | ||
|
|
||
| | Process | Address | Reachable from | | ||
| | --- | --- | --- | | ||
| | Main backend | `localhost:52123` | this machine only | | ||
| | Sync microservice | `localhost:52124` | this machine only | | ||
| | Share server | `0.0.0.0:52125` | any device that can route here | | ||
|
|
||
| The share server is a **second FastAPI app running inside the backend process**. It starts on demand when the first share is created and stops when the last one is revoked, so no port is left open while nothing is shared. If `52125` is busy it tries the next few ports. | ||
|
|
||
| The main backend is never bound beyond localhost, because it exposes shutdown, delete and metadata routes that must not be reachable by other devices. The share server only ever exposes `/s/{token}` and the two media routes beneath it. | ||
|
|
||
| Active shares live **in memory only**. There is no table and no file: quitting PictoPy ends every share, and no token is ever written to disk. | ||
|
|
||
| ## Sharing an album by hand | ||
|
|
||
| ### Find an album id | ||
|
|
||
| ```bash | ||
| curl http://localhost:52123/albums/ | ||
| ``` | ||
|
|
||
| ### Start a share | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:52123/share/albums/<album_id> -H "Content-Type: application/json" -d "{}" | ||
| ``` | ||
|
|
||
| Add an expiry in minutes if you want one: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:52123/share/albums/<album_id> -H "Content-Type: application/json" -d "{\"expires_in_minutes\": 120}" | ||
| ``` | ||
|
|
||
| The response carries the token and one candidate URL per network interface: | ||
|
|
||
| ```json | ||
| { | ||
| "success": true, | ||
| "message": "Album is now shared on the local network", | ||
| "data": { | ||
| "token": "<share_token>", | ||
| "album_name": "The Oddyseys", | ||
| "image_count": 18, | ||
| "port": 52125, | ||
| "urls": [ | ||
| { "interface": "Wi-Fi", "ip": "10.170.93.60", "url": "http://10.170.93.60:52125/s/<share_token>" } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Pick the right URL | ||
|
|
||
| The list is ranked best-guess first — the interface holding the default route wins, virtual adapters such as VMware and WSL sort last, and disconnected interfaces sort below live ones. A machine with virtual adapters can easily surface four candidates, so **the first entry is a ranked guess, not a guarantee**. If the phone cannot load one, try the next. | ||
|
|
||
| You can inspect the ranking without creating a share: | ||
|
|
||
| ```bash | ||
| curl http://localhost:52123/share/interfaces | ||
| ``` | ||
|
|
||
| ### Open it | ||
|
|
||
| Put the chosen URL into the phone's browser, on the same Wi-Fi. The page needs no app and no account. | ||
|
|
||
| ### List and revoke | ||
|
|
||
| ```bash | ||
| curl http://localhost:52123/share/ | ||
| ``` | ||
|
|
||
| ```bash | ||
| curl -X DELETE http://localhost:52123/share/<token> | ||
| ``` | ||
|
|
||
| Revoking the last share stops the network listener entirely. | ||
|
|
||
| ## When the phone cannot connect | ||
|
|
||
| Work through these in order. | ||
|
|
||
| **Check the firewall profile, not just the rule.** Windows commonly creates *Private*-only rules, and most Wi-Fi networks are classified *Public*. A Private-only rule on a Public network fails silently. Rules are per-binary rather than per-port, so a rule covering the | ||
| Python interpreter or `PictoPy_Server` covers whatever port sharing lands on. | ||
|
|
||
| ```powershell | ||
| Get-NetConnectionProfile | Select-Object Name, NetworkCategory | ||
| ``` | ||
|
|
||
| **Confirm both devices are on the same network.** A laptop on wired Ethernet and a phone on Wi-Fi are often on different subnets. Some networks route between them and some deliberately do not. | ||
|
|
||
| **Suspect the network itself.** Many networks block device-to-device traffic — this is called AP or client isolation, and it is close to universal on guest Wi-Fi and common on corporate networks. If it is enabled, nothing on the PictoPy side can work around it. | ||
|
|
||
| To tell an isolated network apart from a broken setup, use a phone hotspot: connect the laptop to the phone's hotspot and share again. If it works there, the code is fine and the other network is blocking peer traffic. USB tethering does the same job and is more reliable still. | ||
|
|
||
| ## Internet mode | ||
|
|
||
| Everything above is the local-network path. Reaching a share from outside the network is handled entirely on the Tauri side rather than in this backend: `frontend/src-tauri/src/services/tunnel.rs` spawns the system `ssh` client as a reverse tunnel to the share port, parses the assigned URL from its output, and closes it when PictoPy exits. | ||
|
|
||
| The backend needs no changes for this — the share server is already bound to `0.0.0.0`, so a tunnel forwarding to its port reaches it exactly as a phone on the LAN would. Nothing about tokens, passwords or expiry differs between the two modes. | ||
|
|
||
| ## Running the tests | ||
|
|
||
| ```bash | ||
| cd backend && pytest tests/test_share_routes.py tests/test_share_registry.py tests/test_network_utils.py | ||
| ``` | ||
|
|
||
| These cover token issue, expiry and revocation, the interface ranking, and the security invariants — most importantly that an image belonging to a different album cannot be fetched through a share token, and that the share server exposes nothing beyond `/s/{token}`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # Sharing Albums | ||
|
|
||
| Hand someone a link to one of your albums. They open it in an ordinary browser — no account, no app, nothing to install. | ||
|
|
||
| Your photos are never *stored* anywhere but your own machine. They are read off your disk and sent to whoever opens the link, for as long as you leave the share running. PictoPy uploads nothing, and keeps no copy on any server. | ||
|
|
||
| Where they *travel* depends on which mode you pick. On your own network they go straight to the other device. Over the internet they pass through a relay run by someone else, which can read them on the way past — that mode is described in full below. | ||
|
|
||
| Two things are true of every share: | ||
|
|
||
| - **It only works while PictoPy is running.** Close the app and the link stops working immediately. | ||
| - **Nothing is stored anywhere else.** There is no copy on a server to worry about. | ||
|
|
||
| ## The two modes | ||
|
|
||
| When you share an album you choose where the link should work from. | ||
|
|
||
| | | This network | Internet | | ||
| | --- | --- | --- | | ||
| | Who can open it | anyone on your Wi-Fi with the link | anyone with the link | | ||
| | Photos pass through | nothing — device to device | a relay that can read them | | ||
| | Connection | plain HTTP, not encrypted | HTTPS, but decrypted at the relay | | ||
| | Needs internet | no | yes | | ||
| | Speed | your Wi-Fi | your home upload speed | | ||
|
|
||
| **This network** is the default and the private one. Use it when the person is in the same house, office or café as you. | ||
|
|
||
| **Internet** is for when they are not. It comes with a real trade-off, described below — worth reading once before you use it. | ||
|
|
||
| ## How "this network" works | ||
|
|
||
| The link goes straight from your machine to theirs. Nothing in between, nobody else involved. | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| P["📱 Their phone<br/>same Wi-Fi"] -- "http://192.168.1.4:52125" --> R{{"🛜 Your router"}} | ||
| R --> M["💻 Your machine<br/>PictoPy reads the photo"] | ||
| ``` | ||
|
|
||
| No outside company is involved, which is what makes this the private option. Two things are still worth knowing: | ||
|
|
||
| - **Anyone on the network who has the link can open it.** The link is not tied to a person, so it works from any device that can reach your machine. | ||
| - **The connection is plain HTTP, not encrypted.** On your own home Wi-Fi that is rarely a concern. On a shared or public network, someone able to watch the traffic could read the photos as they pass, so add a password or prefer a network you trust. | ||
|
|
||
| The catch is that both devices must be on the same network, and **some networks refuse to let their own devices talk to each other**. Guest Wi-Fi almost always does this, and plenty of home routers do too. If the link never loads, that is usually why — see [When the link does not load](#when-the-link-does-not-load). | ||
|
|
||
| ## Internet mode | ||
|
|
||
| Internet mode makes your machine reachable from outside by opening a tunnel through a relay service. Your machine still serves every photo; the relay just passes traffic along. | ||
|
|
||
| ```mermaid | ||
| flowchart TB | ||
| P["📱 Their phone<br/>anywhere in the world"] | ||
| R["☁️ Relay service<br/>localhost.run"] | ||
| S["🔒 Encrypted tunnel"] | ||
| M["💻 Your machine<br/>PictoPy reads the photo off your disk"] | ||
|
|
||
| P -- "HTTPS request" --> R | ||
| R --> S | ||
| S --> M | ||
| M -. "the photo travels back the same way" .-> P | ||
|
|
||
| style R fill:#fde68a,stroke:#d97706,color:#000 | ||
| ``` | ||
|
|
||
| ### What this means for your privacy | ||
|
|
||
| **The relay can see your photos.** This is the part worth understanding. The connection is encrypted from their phone to the relay, and encrypted again from the relay to you — but the relay sits in the middle and handles your images in readable form. It does not keep them, but it could read them as they pass. | ||
|
|
||
| That is the cost of internet mode, and it is why it is never the default. | ||
|
|
||
| **Anyone holding the link can open the album.** The link contains a long random code that nobody can guess, but it is not tied to a person. If it gets forwarded, whoever receives it can open the album. | ||
|
|
||
| **Chat apps open links by themselves.** Paste the link into WhatsApp, Slack, Discord or iMessage and their servers will fetch it to build a preview — without anyone tapping it. Without a password, that preview request sees your album. | ||
|
|
||
| This is why PictoPy turns the password on for you when you choose internet mode. With a password set, anything that fetches the link finds only a password prompt, which reveals nothing at all — not the album name, not how many photos, not a single thumbnail. | ||
|
|
||
| You can turn it off. Just know what it changes. | ||
|
|
||
| ### Speed | ||
|
|
||
| Every photo someone views is sent out from your home internet connection, in full, each time. Home connections upload far more slowly than they download, so a large album will feel slower than Google Photos or iCloud — those serve from their own servers, PictoPy serves from your desk. | ||
|
|
||
| ### Why it works when "this network" does not | ||
|
|
||
| Internet mode makes an **outgoing** connection from your machine to the relay. Nothing has to connect *in* to you, so there is no router setting to change and no port to open. | ||
|
|
||
| That is also why internet mode still works on networks that block device-to-device traffic: your router is happy to let a connection out, it just will not let two of its own devices talk to each other. | ||
|
|
||
| ## Passwords | ||
|
|
||
| A password can be added in either mode. The person opening the link is asked for it before anything loads. | ||
|
|
||
| - The password is optional on your network, and switched on by default for internet mode. | ||
| - Ten wrong guesses puts that share on a short cooldown. | ||
| - The password protects the album *and* the fact that it exists: the prompt page names nothing about it. | ||
|
|
||
| ## Stopping a share | ||
|
|
||
| A share ends when any of these happens: | ||
|
|
||
| - You open the album's share dialog and choose **Stop sharing**. | ||
| - The expiry you picked runs out. | ||
| - **You close PictoPy.** Every share ends, always. | ||
|
|
||
| There is no way for a share to outlive the app, by design. | ||
|
|
||
| ## When the link does not load | ||
|
|
||
| Work through these in order. | ||
|
|
||
| **Check they are on your Wi-Fi.** For "this network" mode both devices have to be on the same network. A laptop on Ethernet and a phone on Wi-Fi are often not. | ||
|
|
||
| **Try one of the other addresses.** If your machine has more than one network address, PictoPy shows a list. The first is a ranked guess, not a certainty — if it does not load, try the next. | ||
|
|
||
| **Check your firewall.** On Windows, a rule created while you were on a *Private* network does not apply once you join a *Public* one, and most Wi-Fi is classified Public. The connection then fails silently. | ||
|
|
||
| **Suspect the network itself.** Many networks block devices from talking to each other — called *AP isolation* or *client isolation*. It is near-universal on guest Wi-Fi. Nothing in PictoPy can work around it. | ||
|
|
||
| To tell an isolated network from a broken setup, turn on your phone's hotspot, connect your computer to it, and share again. If it works there, PictoPy is fine and the other network was blocking it. **Internet mode also solves this**, since it does not rely on the two devices reaching each other directly. | ||
|
|
||
| ## For developers | ||
|
|
||
| The API, the process layout and the security invariants are documented in [Album Sharing](../backend/backend_python/album-sharing.md). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| pub mod tunnel; | ||
|
|
||
| use tauri::path::BaseDirectory; | ||
| use tauri::Manager; | ||
|
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.