remote-fs-browser 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- remote_fs_browser-0.2.0/LICENSE +21 -0
- remote_fs_browser-0.2.0/PKG-INFO +252 -0
- remote_fs_browser-0.2.0/README.md +228 -0
- remote_fs_browser-0.2.0/pyproject.toml +34 -0
- remote_fs_browser-0.2.0/setup.cfg +4 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/__init__.py +11 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/__main__.py +8 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/backends.py +182 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/cli.py +162 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/defaults.py +91 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/discovery.py +150 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/hostnames.py +64 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/http.py +347 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/nfs.py +151 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/policy.py +57 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/sessions.py +307 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/store.py +115 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/web/browser.js +898 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/web/demo.js +75 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser/web/index.html +115 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/PKG-INFO +252 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/SOURCES.txt +32 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/dependency_links.txt +1 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/entry_points.txt +3 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/requires.txt +17 -0
- remote_fs_browser-0.2.0/src/remote_fs_browser.egg-info/top_level.txt +1 -0
- remote_fs_browser-0.2.0/tests/test_cli.py +104 -0
- remote_fs_browser-0.2.0/tests/test_core.py +136 -0
- remote_fs_browser-0.2.0/tests/test_defaults.py +73 -0
- remote_fs_browser-0.2.0/tests/test_hostnames.py +84 -0
- remote_fs_browser-0.2.0/tests/test_http.py +114 -0
- remote_fs_browser-0.2.0/tests/test_native.py +12 -0
- remote_fs_browser-0.2.0/tests/test_security.py +51 -0
- remote_fs_browser-0.2.0/tests/test_store.py +55 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 remote-fs-browser contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remote-fs-browser
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: One remote API for local folders, SMB shares and NFS exports
|
|
5
|
+
Author: remote-fs-browser contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
11
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
12
|
+
Requires-Dist: psutil<8,>=6
|
|
13
|
+
Requires-Dist: cryptography>=42
|
|
14
|
+
Requires-Dist: smbprotocol<2,>=1.17
|
|
15
|
+
Requires-Dist: impacket<1,>=0.12; sys_platform != "win32"
|
|
16
|
+
Provides-Extra: smb-enum
|
|
17
|
+
Requires-Dist: impacket<1,>=0.12; extra == "smb-enum"
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
21
|
+
Requires-Dist: httpx>=0.27; extra == "test"
|
|
22
|
+
Requires-Dist: build>=1; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Remote filesystem browser
|
|
26
|
+
|
|
27
|
+
**Browse local, SMB and NFS storage over HTTP from another machine.**
|
|
28
|
+
|
|
29
|
+
Install it on any machine inside a network, then browse the storage that machine can see from anywhere you can reach its HTTP port. A Mac mini, a workstation, a server or a homelab node becomes a small read-only storage window: it shows its own disks and mounted volumes, and the SMB shares and NFS exports it can reach, without mounting anything, syncing anything or giving the viewer direct access to the NAS.
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
This Computer
|
|
33
|
+
├── Home
|
|
34
|
+
├── Mounted volumes
|
|
35
|
+
└── Attached storage
|
|
36
|
+
|
|
37
|
+
SMB
|
|
38
|
+
├── NAS
|
|
39
|
+
├── Windows shares
|
|
40
|
+
└── Other SMB servers
|
|
41
|
+
|
|
42
|
+
NFS
|
|
43
|
+
├── NAS exports
|
|
44
|
+
└── Server exports
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
"The whole network" means what that host can reach and what its policy and credentials permit. That is the useful bit: the browser works from the network perspective of the machine you installed it on.
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Choose an installation method below, then run `remotefs serve`.
|
|
54
|
+
|
|
55
|
+
### Homebrew (macOS)
|
|
56
|
+
|
|
57
|
+
The Homebrew package is pending publication: the formula in this repository still needs its release checksum and publication to the tap. Once published, install with [Homebrew](https://brew.sh/):
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
brew tap mightymorgs/tap
|
|
61
|
+
brew install mightymorgs/tap/remotefs
|
|
62
|
+
remotefs serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The formula installs Python and libnfs as dependencies. To run it in the background instead of keeping a terminal open:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
brew services start remotefs
|
|
69
|
+
remotefs --print-token
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `brew services stop remotefs` to stop it. To update, run `brew update` followed by `brew upgrade mightymorgs/tap/remotefs`. See the [Homebrew tap documentation](https://docs.brew.sh/Taps) for how third-party packages are installed.
|
|
73
|
+
|
|
74
|
+
### WinGet (Windows x64)
|
|
75
|
+
|
|
76
|
+
The WinGet package is pending publication: its manifest still needs the release ZIP checksum and acceptance into the WinGet community repository. Once published, run in PowerShell with [WinGet installed](https://learn.microsoft.com/en-us/windows/package-manager/winget/):
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
winget install --id mightymorgs.remotefs --exact --source winget
|
|
80
|
+
remotefs serve
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The package uses a portable Windows executable, so a separate Python installation is not required. If `remotefs` is not found after installation, open a new terminal. To update:
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
winget upgrade --id mightymorgs.remotefs --exact --source winget
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [WinGet install options](https://learn.microsoft.com/en-us/windows/package-manager/winget/install) for the command syntax. Maintainers can find the publication steps for both packages in [Releasing remotefs](packaging/RELEASING.md).
|
|
90
|
+
|
|
91
|
+
### Python (Windows, macOS or Linux)
|
|
92
|
+
|
|
93
|
+
Requires Python 3.11+ and pipx. For a published PyPI release:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
pipx install remote-fs-browser
|
|
97
|
+
remotefs serve
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Before the packages are published, install directly from a checkout of this repository:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
pipx install .
|
|
104
|
+
remotefs serve
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Open the browser
|
|
108
|
+
|
|
109
|
+
Open `http://127.0.0.1:8080/` and sign in with the token printed in the terminal. The first run creates `~/.config/remotefs/config.json` (`%APPDATA%\remotefs\config.json` on Windows) with a random token, readable only by you, and every later run reuses it. `remotefs --print-token` shows it again.
|
|
110
|
+
|
|
111
|
+
With no configuration the service exposes, read-only, on loopback only:
|
|
112
|
+
|
|
113
|
+
- **This Computer**: your home directory and mounted volumes (`/Volumes` on macOS, drive letters on Windows, mounts under `/mnt`, `/media`, `/run/media`, `/srv`, `/data` and `/home` on Linux).
|
|
114
|
+
- **SMB and NFS**: servers in the private subnets of the host's physical interfaces (container, VM and tunnel interfaces are ignored), each narrowed to a /24. Press **Scan network** to probe them, or add a server by name.
|
|
115
|
+
|
|
116
|
+
To reach it from another computer, bind to an interface on purpose:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
remotefs serve --bind 0.0.0.0 --port 8080
|
|
120
|
+
# Or one interface only, for example a Tailscale address:
|
|
121
|
+
remotefs serve --bind 100.82.14.7
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The startup banner lists the URLs, warns that the service is reachable from the network, and reminds you that plain HTTP needs a trusted network or an encrypted tunnel. No firewall rule is opened automatically. Authentication is always required.
|
|
125
|
+
|
|
126
|
+
Narrow or widen what is visible with flags, which replace the defaults rather than adding to them:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
remotefs serve --root /srv/media --root /mnt/backup --allow-network 192.168.1.0/24
|
|
130
|
+
remotefs serve --no-defaults --config /etc/remotefs/config.json # expose only what the config names
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
NFS needs libnfs 6 or newer (included as a Homebrew dependency; for Python installs, use `brew install libnfs` or the platform installers below). Local and SMB browsing work without it. On Windows, listing the shares a server offers needs the optional `remote-fs-browser[smb-enum]` extra, which Windows Defender quarantines during install unless the Python environment is excluded; without it, type the share name and browsing works as usual.
|
|
134
|
+
|
|
135
|
+
## Shortlist
|
|
136
|
+
|
|
137
|
+
**Save folder to shortlist** pins the folder you are viewing, on a local root, an SMB share or an NFS export. Signing in later with the same token shows it under **Shortlist**, and one click reopens it; folders on the same SMB share reuse the credentials you gave when you first saved one. The shortlist lives in `saved.json` beside the config, readable only by you and encrypted under the service token; a different token cannot open it. **Forget** removes one entry, deleting the file removes them all. See [security boundaries](SECURITY.md).
|
|
138
|
+
|
|
139
|
+
## The browser
|
|
140
|
+
|
|
141
|
+
The UI and API share one port and origin. Sign-in is its own screen: it exchanges the token for an HttpOnly, SameSite browser cookie lasting eight hours, and **Sign out** revokes it. The picker shows the host's roots and mapped network locations in a sidebar, the folder listing beside it, and collapses to a sources sheet on phones. Downloads stream through the browser's download manager with HTTP Range support, and tokens never appear in download URLs.
|
|
142
|
+
|
|
143
|
+
- `/?mode=browse` (default): navigate folders and download files.
|
|
144
|
+
- `/?mode=select`: choose a directory and copy its credential-free descriptor, for use by other automation.
|
|
145
|
+
|
|
146
|
+
## System service installs
|
|
147
|
+
|
|
148
|
+
For an always-on service run as root or SYSTEM, from a checkout on the target host:
|
|
149
|
+
|
|
150
|
+
- Linux: `sudo scripts/linux/install.sh /path/to/private-config.json`
|
|
151
|
+
- macOS: `sudo scripts/macos/install.sh /path/to/private-config.json`
|
|
152
|
+
- Windows, elevated PowerShell: `scripts/windows/install.ps1 -Config C:\path\private-config.json`
|
|
153
|
+
|
|
154
|
+
These install into a private prefix, build the pinned libnfs (macOS uses Homebrew's), and register a systemd unit, launchd daemon or Windows startup task running `remotefs serve --no-defaults --config …`, so only the roots and networks in the private config are exposed. Start from `examples/config.example.json`: set `local_roots`, `network_ranges` and a random token of at least 32 characters (`python -c "import secrets; print(secrets.token_urlsafe(48))"`), and keep the file private. An empty list denies that class of access.
|
|
155
|
+
|
|
156
|
+
For Ansible, use `playbooks/<platform>/install.yml` with the `filesystem_hosts` group, `remote_fs_source` (destination checkout directory) and `remote_fs_config` (private config path already on the host). macOS also needs `remote_fs_brew_user`; Windows needs `ansible.windows`. The playbooks copy only public source files. Matching uninstall scripts and playbooks stop and remove the service; `--purge` on Unix or `-Purge` on Windows also removes the private installation directory. Never store credentials or real host configurations in Git.
|
|
157
|
+
|
|
158
|
+
## Python SDK
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import asyncio
|
|
162
|
+
from remote_fs_browser import Browser, Policy
|
|
163
|
+
|
|
164
|
+
async def example():
|
|
165
|
+
policy = Policy(local_roots=['/srv/media'])
|
|
166
|
+
async with Browser(policy) as browser:
|
|
167
|
+
async with await browser.connect({'type': 'local', 'root': '/srv/media'}) as fs:
|
|
168
|
+
print(await fs.list('/'))
|
|
169
|
+
print(await fs.stat('/example.mp4'))
|
|
170
|
+
async for chunk in fs.stream('/example.mp4', offset=1024, length=4096):
|
|
171
|
+
consume(chunk)
|
|
172
|
+
selected = fs.descriptor('/Projects')
|
|
173
|
+
|
|
174
|
+
# Worker processes require the normal multiprocessing main guard on every OS.
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
asyncio.run(example())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`list`, `stat`, `stream`, `descriptor` and `close` have identical interfaces for all backends. Paths inside a session always use `/`, including on Windows. A local connection's `root` remains a native host path such as `C:/Media`. Entries contain `name`, normalized `path`, `type`, `size`, and UTC `modified` time. Listings report `truncated` when the entry limit was hit and `skipped` for names that cannot be addressed safely (for example a colon or backslash in a filename); nothing aborts the listing.
|
|
180
|
+
|
|
181
|
+
Connect to SMB with `{'type':'smb','host':'nas.example','share':'Projects'}` and a separate `credentials={'username':..., 'password':...}` argument. NFS uses `{'type':'nfs','host':'nas.example','export':'/exports/media','version':4}`. Set the permitted network ranges first.
|
|
182
|
+
|
|
183
|
+
The worker is created when connecting to a selected location, not merely when opening the picker. It keeps the protocol connection through subsequent navigation and file reads. `close()` terminates it; abandoned workers exit after the configured idle period (300 seconds by default). Each filesystem operation has a hard deadline (10 seconds by default), so a stuck native call cannot block another session. The reference HTTP service also reaps expired session records.
|
|
184
|
+
|
|
185
|
+
## HTTP API
|
|
186
|
+
|
|
187
|
+
All data endpoints require `Authorization: Bearer <token>` or the browser cookie. The page assets alone are public. Routes are served under `/api/`; the unprefixed forms remain for existing SDK clients.
|
|
188
|
+
|
|
189
|
+
| Method / route | Purpose |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `POST /api/login`, `DELETE /api/login` | Exchange the token for a browser cookie; revoke it |
|
|
192
|
+
| `GET /api/discover?scan=false` | Allowed roots and the `groups` tree; `scan=true` also probes permitted ranges |
|
|
193
|
+
| `POST /api/discover` | Enumerate shares/exports: `{type, host, credentials?}` |
|
|
194
|
+
| `GET /api/saved`, `POST /api/saved`, `DELETE /api/saved/{id}` | Remembered locations for the signed-in principal |
|
|
195
|
+
| `POST /api/sessions` | Connect: `{descriptor, credentials?}`; `descriptor.credential_id` reuses saved credentials |
|
|
196
|
+
| `GET /api/sessions/{id}/list?path=/` | Entries plus `truncated` and `skipped` |
|
|
197
|
+
| `GET /api/sessions/{id}/list?path=/&ndjson=true` | NDJSON response, with `X-Listing-Truncated` and `X-Listing-Skipped` headers |
|
|
198
|
+
| `GET /api/sessions/{id}/stat?path=/file` | Normalized metadata |
|
|
199
|
+
| `GET /api/sessions/{id}/file?path=/file` | Stream bytes; supports a single `Range: bytes=...` header |
|
|
200
|
+
| `GET /api/sessions/{id}/descriptor?path=/folder` | Validated durable directory descriptor |
|
|
201
|
+
| `DELETE /api/sessions/{id}` | Close immediately |
|
|
202
|
+
|
|
203
|
+
File responses use bounded 256 KiB reads. Range requests support explicit, open-ended and suffix ranges; invalid/multiple ranges return 416. Files are served as attachments with `nosniff`. A client disconnect releases its active file handle; the browsing session remains for the configured idle grace period so it can reconnect after a brief interruption. An explicit DELETE closes it immediately.
|
|
204
|
+
|
|
205
|
+
Directory listings are bounded by `max_entries`; NDJSON streams the bounded result to the client. The service does not lazily page a native directory across HTTP requests. It reports truncation rather than silently claiming a full listing. It never recursively indexes a share.
|
|
206
|
+
|
|
207
|
+
## Credentials and descriptors
|
|
208
|
+
|
|
209
|
+
Descriptors never contain credentials. A descriptor can hold `credential_id`; `remotefs serve` resolves it from the remembered locations of the signed-in principal, and an embedding application can supply its own resolver instead.
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{"type":"smb","host":"nas.example","share":"Projects","path":"/Campaigns","credential_id":"media-reader"}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The SDK accepts `Browser(policy, credential_resolver=lambda reference: ...)`. The reference service accepts `create_app(policy, token=..., credential_resolver=lambda principal, reference: ...)` or `saved_locations=SavedLocations(path, token)`; a resolver must check that the principal owns the reference before returning credentials. The SDK and `create_app` persist nothing on their own. A local descriptor contains `type`, native `root`, and a session-relative `path`; NFS retains `host`, `export`, `version` and `path`.
|
|
216
|
+
|
|
217
|
+
To customize authentication, pass `authenticate(request) -> principal` and optionally `authorize(principal, operation, descriptor) -> bool` to `create_app`. Hooks can be async. The default bearer token represents one principal; use per-user hooks for separate users. Sessions and saved locations cannot be read by a different principal. The service never logs credentials or request bodies; access logging is disabled by its CLI.
|
|
218
|
+
|
|
219
|
+
## Frontend
|
|
220
|
+
|
|
221
|
+
Use `frontend/browser.js` directly or the packaged `/browser.js` asset. It defines `<remote-fs-browser>` and exports `RemoteFsClient`. No React, Vue or build system is required.
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
import { RemoteFsClient } from './browser.js'
|
|
225
|
+
const picker = document.querySelector('remote-fs-browser')
|
|
226
|
+
picker.client = new RemoteFsClient('/storage-api', () => ({ Authorization: `Bearer ${token}` }))
|
|
227
|
+
picker.addEventListener('path-selected', event => saveDescriptor(event.detail))
|
|
228
|
+
// Optional: the embedding app saves credentials itself and returns an opaque reference.
|
|
229
|
+
// picker.storeCredentials = async credentials => mySecretStore.save(credentials)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The element fills the box it is given. It renders the shortlist and the host's roots in a sidebar, network devices from an explicit scan, per-host share and export lists, credential entry, nested folders with formatted sizes and dates, in-place errors with retry, expiry reconnect, downloads in browse mode and a live descriptor preview with a Select button in select mode. The `signout` attribute adds a Sign out button that fires a `sign-out` event for the host page to act on. When the service offers `/api/saved` and no `storeCredentials` hook is set, "Save folder to shortlist" stores the current folder there. It closes its session on selection, disconnect or element removal. Keep the service on the same origin or configure a restrictive CORS policy when embedding across origins. The JS client's `file()` returns a Fetch `Response`; consume its `body` as a stream rather than calling `blob()` for large files.
|
|
233
|
+
|
|
234
|
+
## Discovery limits
|
|
235
|
+
|
|
236
|
+
Scan results show the DNS hostname, NetBIOS device name and IP address together whenever the names are available. DNS and NetBIOS are queried independently; devices without either name still show their IP address. Named devices retain a label in the Network sidebar, and connections use the scanned IP address. Both name lookups have deadlines so unavailable name services do not hold up the scan indefinitely.
|
|
237
|
+
|
|
238
|
+
Discovery probes TCP 445/2049 only in explicitly permitted ranges of at most 256 addresses each, and scans at most 1024 candidates per request, reporting when more were permitted. This is portable and requires no SMB1 browser service. Manual hostnames work when discovery cannot cross subnets or VPNs. SMB authentication uses NTLM (including domain-qualified usernames). SMB enumeration uses Impacket's SRVS RPC over SMB2; traversal and streaming use smbprotocol's SMB2/3 session. NFS export enumeration uses mountd and may return no exports on NFSv4-only servers; enter the export manually in that case. NFS uses AUTH_SYS UID/GID behaviour from libnfs and the service account; NFS Kerberos is not configured.
|
|
239
|
+
|
|
240
|
+
This project is a path picker and read-only browser. It does not provision mounts, manage backups, sync files, or abstract cloud object storage. It is a reference service and embedding SDK, not a hardened multi-tenant filesystem sandbox: see [security boundaries](SECURITY.md) and [validation](VALIDATION.md) before exposing it beyond a trusted network.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
pip install -e '.[test]'
|
|
246
|
+
pytest
|
|
247
|
+
python -m build
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Releases are built by `.github/workflows/release.yml`; `packaging/RELEASING.md` describes publishing to PyPI, the Homebrew tap and winget.
|
|
251
|
+
|
|
252
|
+
MIT licensed. Protocol implementations are dependencies, not copied sources: [smbprotocol](https://github.com/jborean93/smbprotocol), [Impacket](https://github.com/fortra/impacket), and [libnfs](https://github.com/sahlberg/libnfs). Libnfs has its own LGPL licensing; installers fetch or build it separately. Preserve its license obligations if distributing a bundled native library.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Remote filesystem browser
|
|
2
|
+
|
|
3
|
+
**Browse local, SMB and NFS storage over HTTP from another machine.**
|
|
4
|
+
|
|
5
|
+
Install it on any machine inside a network, then browse the storage that machine can see from anywhere you can reach its HTTP port. A Mac mini, a workstation, a server or a homelab node becomes a small read-only storage window: it shows its own disks and mounted volumes, and the SMB shares and NFS exports it can reach, without mounting anything, syncing anything or giving the viewer direct access to the NAS.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
This Computer
|
|
9
|
+
├── Home
|
|
10
|
+
├── Mounted volumes
|
|
11
|
+
└── Attached storage
|
|
12
|
+
|
|
13
|
+
SMB
|
|
14
|
+
├── NAS
|
|
15
|
+
├── Windows shares
|
|
16
|
+
└── Other SMB servers
|
|
17
|
+
|
|
18
|
+
NFS
|
|
19
|
+
├── NAS exports
|
|
20
|
+
└── Server exports
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
"The whole network" means what that host can reach and what its policy and credentials permit. That is the useful bit: the browser works from the network perspective of the machine you installed it on.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Choose an installation method below, then run `remotefs serve`.
|
|
30
|
+
|
|
31
|
+
### Homebrew (macOS)
|
|
32
|
+
|
|
33
|
+
The Homebrew package is pending publication: the formula in this repository still needs its release checksum and publication to the tap. Once published, install with [Homebrew](https://brew.sh/):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
brew tap mightymorgs/tap
|
|
37
|
+
brew install mightymorgs/tap/remotefs
|
|
38
|
+
remotefs serve
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The formula installs Python and libnfs as dependencies. To run it in the background instead of keeping a terminal open:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
brew services start remotefs
|
|
45
|
+
remotefs --print-token
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `brew services stop remotefs` to stop it. To update, run `brew update` followed by `brew upgrade mightymorgs/tap/remotefs`. See the [Homebrew tap documentation](https://docs.brew.sh/Taps) for how third-party packages are installed.
|
|
49
|
+
|
|
50
|
+
### WinGet (Windows x64)
|
|
51
|
+
|
|
52
|
+
The WinGet package is pending publication: its manifest still needs the release ZIP checksum and acceptance into the WinGet community repository. Once published, run in PowerShell with [WinGet installed](https://learn.microsoft.com/en-us/windows/package-manager/winget/):
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
winget install --id mightymorgs.remotefs --exact --source winget
|
|
56
|
+
remotefs serve
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The package uses a portable Windows executable, so a separate Python installation is not required. If `remotefs` is not found after installation, open a new terminal. To update:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
winget upgrade --id mightymorgs.remotefs --exact --source winget
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See [WinGet install options](https://learn.microsoft.com/en-us/windows/package-manager/winget/install) for the command syntax. Maintainers can find the publication steps for both packages in [Releasing remotefs](packaging/RELEASING.md).
|
|
66
|
+
|
|
67
|
+
### Python (Windows, macOS or Linux)
|
|
68
|
+
|
|
69
|
+
Requires Python 3.11+ and pipx. For a published PyPI release:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pipx install remote-fs-browser
|
|
73
|
+
remotefs serve
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Before the packages are published, install directly from a checkout of this repository:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pipx install .
|
|
80
|
+
remotefs serve
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Open the browser
|
|
84
|
+
|
|
85
|
+
Open `http://127.0.0.1:8080/` and sign in with the token printed in the terminal. The first run creates `~/.config/remotefs/config.json` (`%APPDATA%\remotefs\config.json` on Windows) with a random token, readable only by you, and every later run reuses it. `remotefs --print-token` shows it again.
|
|
86
|
+
|
|
87
|
+
With no configuration the service exposes, read-only, on loopback only:
|
|
88
|
+
|
|
89
|
+
- **This Computer**: your home directory and mounted volumes (`/Volumes` on macOS, drive letters on Windows, mounts under `/mnt`, `/media`, `/run/media`, `/srv`, `/data` and `/home` on Linux).
|
|
90
|
+
- **SMB and NFS**: servers in the private subnets of the host's physical interfaces (container, VM and tunnel interfaces are ignored), each narrowed to a /24. Press **Scan network** to probe them, or add a server by name.
|
|
91
|
+
|
|
92
|
+
To reach it from another computer, bind to an interface on purpose:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
remotefs serve --bind 0.0.0.0 --port 8080
|
|
96
|
+
# Or one interface only, for example a Tailscale address:
|
|
97
|
+
remotefs serve --bind 100.82.14.7
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The startup banner lists the URLs, warns that the service is reachable from the network, and reminds you that plain HTTP needs a trusted network or an encrypted tunnel. No firewall rule is opened automatically. Authentication is always required.
|
|
101
|
+
|
|
102
|
+
Narrow or widen what is visible with flags, which replace the defaults rather than adding to them:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
remotefs serve --root /srv/media --root /mnt/backup --allow-network 192.168.1.0/24
|
|
106
|
+
remotefs serve --no-defaults --config /etc/remotefs/config.json # expose only what the config names
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
NFS needs libnfs 6 or newer (included as a Homebrew dependency; for Python installs, use `brew install libnfs` or the platform installers below). Local and SMB browsing work without it. On Windows, listing the shares a server offers needs the optional `remote-fs-browser[smb-enum]` extra, which Windows Defender quarantines during install unless the Python environment is excluded; without it, type the share name and browsing works as usual.
|
|
110
|
+
|
|
111
|
+
## Shortlist
|
|
112
|
+
|
|
113
|
+
**Save folder to shortlist** pins the folder you are viewing, on a local root, an SMB share or an NFS export. Signing in later with the same token shows it under **Shortlist**, and one click reopens it; folders on the same SMB share reuse the credentials you gave when you first saved one. The shortlist lives in `saved.json` beside the config, readable only by you and encrypted under the service token; a different token cannot open it. **Forget** removes one entry, deleting the file removes them all. See [security boundaries](SECURITY.md).
|
|
114
|
+
|
|
115
|
+
## The browser
|
|
116
|
+
|
|
117
|
+
The UI and API share one port and origin. Sign-in is its own screen: it exchanges the token for an HttpOnly, SameSite browser cookie lasting eight hours, and **Sign out** revokes it. The picker shows the host's roots and mapped network locations in a sidebar, the folder listing beside it, and collapses to a sources sheet on phones. Downloads stream through the browser's download manager with HTTP Range support, and tokens never appear in download URLs.
|
|
118
|
+
|
|
119
|
+
- `/?mode=browse` (default): navigate folders and download files.
|
|
120
|
+
- `/?mode=select`: choose a directory and copy its credential-free descriptor, for use by other automation.
|
|
121
|
+
|
|
122
|
+
## System service installs
|
|
123
|
+
|
|
124
|
+
For an always-on service run as root or SYSTEM, from a checkout on the target host:
|
|
125
|
+
|
|
126
|
+
- Linux: `sudo scripts/linux/install.sh /path/to/private-config.json`
|
|
127
|
+
- macOS: `sudo scripts/macos/install.sh /path/to/private-config.json`
|
|
128
|
+
- Windows, elevated PowerShell: `scripts/windows/install.ps1 -Config C:\path\private-config.json`
|
|
129
|
+
|
|
130
|
+
These install into a private prefix, build the pinned libnfs (macOS uses Homebrew's), and register a systemd unit, launchd daemon or Windows startup task running `remotefs serve --no-defaults --config …`, so only the roots and networks in the private config are exposed. Start from `examples/config.example.json`: set `local_roots`, `network_ranges` and a random token of at least 32 characters (`python -c "import secrets; print(secrets.token_urlsafe(48))"`), and keep the file private. An empty list denies that class of access.
|
|
131
|
+
|
|
132
|
+
For Ansible, use `playbooks/<platform>/install.yml` with the `filesystem_hosts` group, `remote_fs_source` (destination checkout directory) and `remote_fs_config` (private config path already on the host). macOS also needs `remote_fs_brew_user`; Windows needs `ansible.windows`. The playbooks copy only public source files. Matching uninstall scripts and playbooks stop and remove the service; `--purge` on Unix or `-Purge` on Windows also removes the private installation directory. Never store credentials or real host configurations in Git.
|
|
133
|
+
|
|
134
|
+
## Python SDK
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import asyncio
|
|
138
|
+
from remote_fs_browser import Browser, Policy
|
|
139
|
+
|
|
140
|
+
async def example():
|
|
141
|
+
policy = Policy(local_roots=['/srv/media'])
|
|
142
|
+
async with Browser(policy) as browser:
|
|
143
|
+
async with await browser.connect({'type': 'local', 'root': '/srv/media'}) as fs:
|
|
144
|
+
print(await fs.list('/'))
|
|
145
|
+
print(await fs.stat('/example.mp4'))
|
|
146
|
+
async for chunk in fs.stream('/example.mp4', offset=1024, length=4096):
|
|
147
|
+
consume(chunk)
|
|
148
|
+
selected = fs.descriptor('/Projects')
|
|
149
|
+
|
|
150
|
+
# Worker processes require the normal multiprocessing main guard on every OS.
|
|
151
|
+
if __name__ == '__main__':
|
|
152
|
+
asyncio.run(example())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`list`, `stat`, `stream`, `descriptor` and `close` have identical interfaces for all backends. Paths inside a session always use `/`, including on Windows. A local connection's `root` remains a native host path such as `C:/Media`. Entries contain `name`, normalized `path`, `type`, `size`, and UTC `modified` time. Listings report `truncated` when the entry limit was hit and `skipped` for names that cannot be addressed safely (for example a colon or backslash in a filename); nothing aborts the listing.
|
|
156
|
+
|
|
157
|
+
Connect to SMB with `{'type':'smb','host':'nas.example','share':'Projects'}` and a separate `credentials={'username':..., 'password':...}` argument. NFS uses `{'type':'nfs','host':'nas.example','export':'/exports/media','version':4}`. Set the permitted network ranges first.
|
|
158
|
+
|
|
159
|
+
The worker is created when connecting to a selected location, not merely when opening the picker. It keeps the protocol connection through subsequent navigation and file reads. `close()` terminates it; abandoned workers exit after the configured idle period (300 seconds by default). Each filesystem operation has a hard deadline (10 seconds by default), so a stuck native call cannot block another session. The reference HTTP service also reaps expired session records.
|
|
160
|
+
|
|
161
|
+
## HTTP API
|
|
162
|
+
|
|
163
|
+
All data endpoints require `Authorization: Bearer <token>` or the browser cookie. The page assets alone are public. Routes are served under `/api/`; the unprefixed forms remain for existing SDK clients.
|
|
164
|
+
|
|
165
|
+
| Method / route | Purpose |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `POST /api/login`, `DELETE /api/login` | Exchange the token for a browser cookie; revoke it |
|
|
168
|
+
| `GET /api/discover?scan=false` | Allowed roots and the `groups` tree; `scan=true` also probes permitted ranges |
|
|
169
|
+
| `POST /api/discover` | Enumerate shares/exports: `{type, host, credentials?}` |
|
|
170
|
+
| `GET /api/saved`, `POST /api/saved`, `DELETE /api/saved/{id}` | Remembered locations for the signed-in principal |
|
|
171
|
+
| `POST /api/sessions` | Connect: `{descriptor, credentials?}`; `descriptor.credential_id` reuses saved credentials |
|
|
172
|
+
| `GET /api/sessions/{id}/list?path=/` | Entries plus `truncated` and `skipped` |
|
|
173
|
+
| `GET /api/sessions/{id}/list?path=/&ndjson=true` | NDJSON response, with `X-Listing-Truncated` and `X-Listing-Skipped` headers |
|
|
174
|
+
| `GET /api/sessions/{id}/stat?path=/file` | Normalized metadata |
|
|
175
|
+
| `GET /api/sessions/{id}/file?path=/file` | Stream bytes; supports a single `Range: bytes=...` header |
|
|
176
|
+
| `GET /api/sessions/{id}/descriptor?path=/folder` | Validated durable directory descriptor |
|
|
177
|
+
| `DELETE /api/sessions/{id}` | Close immediately |
|
|
178
|
+
|
|
179
|
+
File responses use bounded 256 KiB reads. Range requests support explicit, open-ended and suffix ranges; invalid/multiple ranges return 416. Files are served as attachments with `nosniff`. A client disconnect releases its active file handle; the browsing session remains for the configured idle grace period so it can reconnect after a brief interruption. An explicit DELETE closes it immediately.
|
|
180
|
+
|
|
181
|
+
Directory listings are bounded by `max_entries`; NDJSON streams the bounded result to the client. The service does not lazily page a native directory across HTTP requests. It reports truncation rather than silently claiming a full listing. It never recursively indexes a share.
|
|
182
|
+
|
|
183
|
+
## Credentials and descriptors
|
|
184
|
+
|
|
185
|
+
Descriptors never contain credentials. A descriptor can hold `credential_id`; `remotefs serve` resolves it from the remembered locations of the signed-in principal, and an embedding application can supply its own resolver instead.
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{"type":"smb","host":"nas.example","share":"Projects","path":"/Campaigns","credential_id":"media-reader"}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The SDK accepts `Browser(policy, credential_resolver=lambda reference: ...)`. The reference service accepts `create_app(policy, token=..., credential_resolver=lambda principal, reference: ...)` or `saved_locations=SavedLocations(path, token)`; a resolver must check that the principal owns the reference before returning credentials. The SDK and `create_app` persist nothing on their own. A local descriptor contains `type`, native `root`, and a session-relative `path`; NFS retains `host`, `export`, `version` and `path`.
|
|
192
|
+
|
|
193
|
+
To customize authentication, pass `authenticate(request) -> principal` and optionally `authorize(principal, operation, descriptor) -> bool` to `create_app`. Hooks can be async. The default bearer token represents one principal; use per-user hooks for separate users. Sessions and saved locations cannot be read by a different principal. The service never logs credentials or request bodies; access logging is disabled by its CLI.
|
|
194
|
+
|
|
195
|
+
## Frontend
|
|
196
|
+
|
|
197
|
+
Use `frontend/browser.js` directly or the packaged `/browser.js` asset. It defines `<remote-fs-browser>` and exports `RemoteFsClient`. No React, Vue or build system is required.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
import { RemoteFsClient } from './browser.js'
|
|
201
|
+
const picker = document.querySelector('remote-fs-browser')
|
|
202
|
+
picker.client = new RemoteFsClient('/storage-api', () => ({ Authorization: `Bearer ${token}` }))
|
|
203
|
+
picker.addEventListener('path-selected', event => saveDescriptor(event.detail))
|
|
204
|
+
// Optional: the embedding app saves credentials itself and returns an opaque reference.
|
|
205
|
+
// picker.storeCredentials = async credentials => mySecretStore.save(credentials)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The element fills the box it is given. It renders the shortlist and the host's roots in a sidebar, network devices from an explicit scan, per-host share and export lists, credential entry, nested folders with formatted sizes and dates, in-place errors with retry, expiry reconnect, downloads in browse mode and a live descriptor preview with a Select button in select mode. The `signout` attribute adds a Sign out button that fires a `sign-out` event for the host page to act on. When the service offers `/api/saved` and no `storeCredentials` hook is set, "Save folder to shortlist" stores the current folder there. It closes its session on selection, disconnect or element removal. Keep the service on the same origin or configure a restrictive CORS policy when embedding across origins. The JS client's `file()` returns a Fetch `Response`; consume its `body` as a stream rather than calling `blob()` for large files.
|
|
209
|
+
|
|
210
|
+
## Discovery limits
|
|
211
|
+
|
|
212
|
+
Scan results show the DNS hostname, NetBIOS device name and IP address together whenever the names are available. DNS and NetBIOS are queried independently; devices without either name still show their IP address. Named devices retain a label in the Network sidebar, and connections use the scanned IP address. Both name lookups have deadlines so unavailable name services do not hold up the scan indefinitely.
|
|
213
|
+
|
|
214
|
+
Discovery probes TCP 445/2049 only in explicitly permitted ranges of at most 256 addresses each, and scans at most 1024 candidates per request, reporting when more were permitted. This is portable and requires no SMB1 browser service. Manual hostnames work when discovery cannot cross subnets or VPNs. SMB authentication uses NTLM (including domain-qualified usernames). SMB enumeration uses Impacket's SRVS RPC over SMB2; traversal and streaming use smbprotocol's SMB2/3 session. NFS export enumeration uses mountd and may return no exports on NFSv4-only servers; enter the export manually in that case. NFS uses AUTH_SYS UID/GID behaviour from libnfs and the service account; NFS Kerberos is not configured.
|
|
215
|
+
|
|
216
|
+
This project is a path picker and read-only browser. It does not provision mounts, manage backups, sync files, or abstract cloud object storage. It is a reference service and embedding SDK, not a hardened multi-tenant filesystem sandbox: see [security boundaries](SECURITY.md) and [validation](VALIDATION.md) before exposing it beyond a trusted network.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
pip install -e '.[test]'
|
|
222
|
+
pytest
|
|
223
|
+
python -m build
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Releases are built by `.github/workflows/release.yml`; `packaging/RELEASING.md` describes publishing to PyPI, the Homebrew tap and winget.
|
|
227
|
+
|
|
228
|
+
MIT licensed. Protocol implementations are dependencies, not copied sources: [smbprotocol](https://github.com/jborean93/smbprotocol), [Impacket](https://github.com/fortra/impacket), and [libnfs](https://github.com/sahlberg/libnfs). Libnfs has its own LGPL licensing; installers fetch or build it separately. Preserve its license obligations if distributing a bundled native library.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "remote-fs-browser"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "One remote API for local folders, SMB shares and NFS exports"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "remote-fs-browser contributors"}]
|
|
13
|
+
dependencies = ["fastapi>=0.115,<1", "uvicorn>=0.30,<1", "psutil>=6,<8", "cryptography>=42", "smbprotocol>=1.17,<2",
|
|
14
|
+
# Share enumeration only. Windows Defender quarantines impacket's bundled example scripts
|
|
15
|
+
# and aborts pip on Windows, so it is an opt-in extra there.
|
|
16
|
+
"impacket>=0.12,<1; sys_platform != 'win32'"]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
smb-enum = ["impacket>=0.12,<1"]
|
|
20
|
+
test = ["pytest>=8", "pytest-asyncio>=0.24", "httpx>=0.27", "build>=1"]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
remotefs = "remote_fs_browser.cli:main"
|
|
24
|
+
# Deprecated alias kept for existing service units; remove in 0.3.
|
|
25
|
+
remote-fs-browser = "remote_fs_browser.http:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
remote_fs_browser = ["web/*.js", "web/*.html"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Cross-platform remote filesystem browsing."""
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
from .policy import Policy
|
|
4
|
+
from .sessions import Browser, FilesystemSession
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version('remote-fs-browser')
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = '0.0.0'
|
|
10
|
+
|
|
11
|
+
__all__ = ['Browser', 'FilesystemSession', 'Policy', '__version__']
|